筛选、切片

  • 在上一篇中你已看到了,流让你从外部迭代转向内部迭代。这样,你就用不着写代码来显式地管理数据集合的迭代(外部迭代)了。
  • 看看如何选择流中的元素:用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截短至指定长度。
  • 使用谓词筛选
  • Streams支持filter方法,该操作会接受一个谓词(一个返回boolean的函数)作为参数,并且返回一个包括所有符合谓词元素的流
  • 例如:筛选出所有素菜,创建一张素食菜单 ```java public class Test4 { public static void main(String[] args) {
    1. List<Dish> menu = DishClient.getMenu();
    2. List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(Collectors.toList());
    } }
  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1806904/1646352796892-f902789c-5ad0-4cdf-994c-6e01f5f0f42a.png#clientId=uf2fcd9f9-e7fb-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=350&id=blNZg&margin=%5Bobject%20Object%5D&name=image.png&originHeight=700&originWidth=1610&originalType=binary&ratio=1&rotation=0&showTitle=false&size=136580&status=done&style=none&taskId=u4f3fd103-e063-43d3-9173-bff8c80813a&title=&width=805)
  2. - **筛选各异的元素**
  3. - 流还支持一个叫做distinct的方法,他会对流进行去重,根据hashcodeequals来判断是否是同一个对象
  4. ```java
  5. public class Test5 {
  6. public static void main(String[] args) {
  7. List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
  8. numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);
  9. }
  10. }

image.png

  • 截断流
  • 流支持limit(n)方法,该方法会返回一个不超过给定长度的流,所需的长度作为参数传递给limit。如果流是有序的,则最多会返回前n个元素。比如,你可以建立一个List,选出热量超过300卡路里的头三道菜的名字

    1. public class Test6 {
    2. public static void main(String[] args) {
    3. List<String> names = DishClient.getMenu()
    4. .stream()
    5. .filter(d -> d.getCalories() > 300)
    6. .map(Dish::getName)
    7. .limit(3)
    8. .collect(Collectors.toList());
    9. System.out.println(names);
    10. }
    11. }

    image.png

  • limit也可以用在无序流上,比如源是一个Set。这种情况下,limit的结果不会以任何顺序排列。

  • 跳过元素
  • 流还支持skip(n)方法,返回一个扔掉了前n个元素的流。如果流中元素不足n个,则返回一个空流。请注意,limit(n)和skip(n)是互补的。 ```java public class Test7 { public static void main(String[] args) {
    1. List<String> names = DishClient.getMenu()
    2. .stream()
    3. .filter(d -> d.getCalories() > 300)
    4. .map(Dish::getName)
    5. .skip(2)
    6. .collect(Collectors.toList());
    7. System.out.println(names);
    } }
  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1806904/1646353993991-7bf04d0c-3925-465b-a62a-1365e79465e6.png#clientId=uf2fcd9f9-e7fb-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=436&id=cwaCZ&margin=%5Bobject%20Object%5D&name=image.png&originHeight=872&originWidth=1590&originalType=binary&ratio=1&rotation=0&showTitle=false&size=154712&status=done&style=none&taskId=u1d88cef5-45c1-4908-8d51-a1df86244c9&title=&width=795)
  2. <a name="c4rR7"></a>
  3. # 映射
  4. - 一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过mapflatMap方法提供了类似的工具。
  5. - **对流中每一个元素应用函数**
  6. - 流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。如下,则是取出菜名。
  7. ```java
  8. public class Test8 {
  9. public static void main(String[] args) {
  10. List<String> names = DishClient.getMenu()
  11. .stream()
  12. .map(Dish::getName)
  13. .collect(Collectors.toList());
  14. System.out.println(names);
  15. }
  16. }
  • 因为getName方法返回一个String,所以map方法输出的流的类型就是Stream
  • 给定一个单词列表,你想要返回另一个列表,显示每个单词中有几个字母。 ```java public class Test9 { public static void main(String[] args) {
    1. List<String> words = Arrays.asList("Java 8", "icanci", "ldl", "in", "Action");
    2. List<Integer> wordLengths = words.stream().map(String::length).collect(Collectors.toList());
    } }
  1. - 现在需要:计算菜名的长度
  2. ```java
  3. public class Test10 {
  4. public static void main(String[] args) {
  5. List<Integer> nameLength = DishClient.getMenu()
  6. .stream()
  7. .map(Dish::getName)
  8. .map(String::length)
  9. .collect(Collectors.toList());
  10. System.out.println(nameLength);
  11. }
  12. }
  • 流的扁平化
  • 对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表[“Hello”, “World”],你想要返回列表[“H”, “e”, “l”, “o”, “W”, “r”, “d”] ```java public class Test11 { public static void main(String[] args) {
    1. List<String> words = Arrays.asList("Hello", "World");
    2. List<String[]> collect = words.stream().map(word -> word.split("")).distinct().collect(Collectors.toList());
    3. System.out.println(collect);
    } }
  1. - 这种方式是有问题的,map实际上返回的是Stream<String[]>,请看下图
  2. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/1806904/1646357655130-f93725f8-f1eb-4914-8040-6d87f5551641.png#clientId=uf2fcd9f9-e7fb-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=457&id=zN91x&margin=%5Bobject%20Object%5D&name=image.png&originHeight=914&originWidth=1796&originalType=binary&ratio=1&rotation=0&showTitle=false&size=164214&status=done&style=none&taskId=u1a456277-6909-4076-bcce-db9d6f086a4&title=&width=898)
  3. - 这样的结果就不是我们想要的结果了,那么怎么解决呢?
  4. - 那就是`让String[]流再成为流`什么意思呢,就是让 `Stream<String[]> 每个元素平铺成 Stream<String>`
  5. - 我们可以使用flatMap来解决这个问题,但是在解决这个问题之前,先尝试用另外一种方法
  6. - **尝试使用mapArrays.stream()**
  7. ```java
  8. public class Test12 {
  9. public static void main(String[] args) {
  10. List<String> words = Arrays.asList("Hello", "World");
  11. List<Stream<String>> collect = words
  12. .stream()
  13. .map(word -> word.split(""))
  14. .map(Arrays::stream)
  15. .distinct()
  16. .collect(Collectors.toList());
  17. System.out.println(collect);
  18. }
  19. }
  • 结果显然,这是又包装了一层。
  • 使用flatMap来解决这个问题

    1. public class Test13 {
    2. public static void main(String[] args) {
    3. List<String> words = Arrays.asList("Hello", "World");
    4. List<String> collect = words
    5. .stream()
    6. .map(word -> word.split(""))
    7. .flatMap(Arrays::stream)
    8. .distinct()
    9. .collect(Collectors.toList());
    10. System.out.println(collect);
    11. }
    12. }
  • 使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。

image.png

  • 一言以蔽之,flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。
  • 来练习一手
  • 给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?例如,给定[1, 2, 3, 4, 5],应该返回[1, 4, 9, 16, 25] ```java public class Test14 { public static void main(String[] args) {
    1. List<Integer> numbers = Arrays.asList(1,2,3,4,5 );
    2. List<Integer> res = numbers
    3. .stream()
    4. .map(num->num*num)
    5. .collect(Collectors.toList());
    6. System.out.println(res);
    } }
  1. - 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]
  2. - 你可以使用两个map来迭代这两个列表,并生成数对。但这样会返回一个Stream-<Stream<Integer[]>>。你需要让生成的流扁平化,以得到一个Stream<Integer[]>
  3. ```java
  4. public class Test15 {
  5. public static void main(String[] args) {
  6. List<Integer> numbers1 = Arrays.asList(1, 2, 3);
  7. List<Integer> numbers2 = Arrays.asList(4, 5);
  8. List<int[]> res = numbers1
  9. .stream()
  10. .flatMap(i -> numbers2.stream().map(j -> new int[] { i, j }))
  11. .collect(Collectors.toList());
  12. System.out.println(res);
  13. }
  14. }
  • 如何扩展前一个例子,只返回总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的 ```java public class Test16 { public static void main(String[] args) {
    1. List<Integer> numbers1 = Arrays.asList(1, 2, 3);
    2. List<Integer> numbers2 = Arrays.asList(4, 5);
    3. List<int[]> res = numbers1
    4. .stream()
    5. .flatMap(i -> numbers2.stream().filter(j -> (i + j) % 3 == 0).map(j -> new int[] { i, j }))
    6. .collect(Collectors.toList());
    7. System.out.println(res);
    } }
  1. <a name="iUsrM"></a>
  2. # 查找、匹配
  3. - 另一个常见的数据处理套路是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findFirst和findAny方法提供了这样的工具。
  4. - 这些方法见名知道意思:allMatch全部匹配、anyMatch部分匹配、noneMatch都不匹配、findFirst寻找第一个、findAny返回任意元素。
  5. - **检查谓词是否至少匹配一个元素:**anyMatch方法返回一个boolean,因此是一个终端操作
  6. ```java
  7. public class Test1 {
  8. public static void main(String[] args) {
  9. List<Dish> menu = DishClient.getMenu();
  10. if (menu.stream().anyMatch(Dish::isVegetarian)) {
  11. System.out.println("Test1.main");
  12. }
  13. }
  14. }
  • 检查谓词是否匹配所有元素:allMatch方法返回一个boolean,因此是一个终端操作 ```java public class Test2 { public static void main(String[] args) {
    1. List<Dish> menu = DishClient.getMenu();
    2. if (menu.stream().allMatch(Dish::isVegetarian)) {
    3. System.out.println("Test1.main");
    4. }
    } }
  1. - **检查谓词是否所有元素都不匹配:**noneMatch方法返回一个boolean,因此是一个终端操作
  2. ```java
  3. public class Test3 {
  4. public static void main(String[] args) {
  5. List<Dish> menu = DishClient.getMenu();
  6. if (menu.stream().noneMatch(Dish::isVegetarian)) {
  7. System.out.println("Test1.main");
  8. }
  9. }
  10. }
  • anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中&&和||运算符短路在流中的版本。
  • 短路求值
  • 有些操作不需要处理整个流就能得到结果。例如,假设你需要对一个用and连起来的大布尔表达式求值。不管表达式有多长,你只需找到一个表达式为false,就可以推断整个表达式将返回false,所以用不着计算整个表达式。这就是短路。
  • 对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。
  • 查找元素 ```java public class Test4 { public static void main(String[] args) {
    1. List<Dish> menu = DishClient.getMenu();
    2. Optional<Dish> optional = menu.stream().filter(Dish::isVegetarian).findAny();
    } }
  1. - 流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。
  2. - **Optional简介**
  3. - Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或不存在。在上面的代码中,findAny可能什么元素都没找到。
  4. - Java 8的库设计人员引入了Optional<T>,这样就不用返回众所周知容易出问题的null了。
  5. - 具象的API如下
  6. - isPresent()将在Optional包含值的时候返回true,否则返回false
  7. - ifPresent(Consumer<T> block)会在值存在的时候执行给定的代码块。它让你传递一个接收T类型参数,并返回voidLambda表达式。
  8. - T get()会在值存在时返回值,否则抛出一个NoSuchElement异常。
  9. - T orElse(T other)会在值存在时返回值,否则返回一个默认值。
  10. ```java
  11. public class Test4 {
  12. public static void main(String[] args) {
  13. List<Dish> menu = DishClient.getMenu();
  14. menu.stream().filter(Dish::isVegetarian).findAny().ifPresent(d -> System.out.println("Test4.main"));
  15. }
  16. }
  • 查找第一个元素 ```java public class Test5 { public static void main(String[] args) {
    1. List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
    2. Optional<Integer> optional = someNumbers.stream().map(x -> x * x).filter(x -> x % 3 == 0).findFirst();
    3. if (optional.isPresent()){
    4. System.out.println(optional.get());
    5. }
    } }
  1. - **何时使用findFirstfindAny:**你可能会想,为什么会同时有findFirstfindAny呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流时限制较少。
  2. <a name="exZ2n"></a>
  3. # 归约
  4. - 到目前为止,你见到过的终端操作都是返回一个booleanallMatch之类的)、voidforEach)或Optional对象(findAny等)。你也见过了使用collect来将流中的所有元素组合成一个List
  5. - 什么是归约:如计算菜单中的总卡路里、菜单中卡路里最高的菜是哪一个。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。
  6. - **元素求和**
  7. - for-each方法
  8. ```java
  9. public class Test6 {
  10. public static void main(String[] args) {
  11. List<Integer> numbers = Arrays.asList(1, 2, 3, 3, 4, 5, 6, 8);
  12. int sum = 0;
  13. for (Integer number : numbers) {
  14. sum += number;
  15. }
  16. System.out.println(sum);
  17. }
  18. }
  • numbers中的每个元素都用加法运算符反复迭代来得到结果。通过反复使用加法,你把一个数字列表归约成了一个数字。这段代码中有两个参数
    • 总和变量的初始值,在这里是0
    • 将列表中所有元素结合在一起的操作,在这里是+。
  • 要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是reduce操作的用武之地,它对这种重复应用的模式做了抽象。 ```java public class Test7 { public static void main(String[] args) {
    1. List<Integer> numbers = Arrays.asList(1, 2, 3, 3, 4, 5, 6, 8);
    2. Integer sum = numbers.stream().reduce(0, (a, b) -> a + b);
    3. System.out.println(sum);
    } }
  1. - reduce接受2个参数:一个初始值,这里是0;一个BinaryOperator<T>来将两个元素结合起来产生一个新值,这里我们用的是lambda (a, b)-> a+b
  2. - 而相乘:如下
  3. ```java
  4. public class Test8 {
  5. public static void main(String[] args) {
  6. List<Integer> numbers = Arrays.asList(1, 2, 3, 3, 4, 5, 6, 8);
  7. Integer sum = numbers.stream().reduce(0, (a, b) -> a * b);
  8. System.out.println(sum);
  9. }
  10. }
  • reduce操作示意图:使用reduce来对流中的数字求和

image.png

  • 让我们深入研究一下reduce操作是如何对一个数字流求和的。首先,0作为Lambda(a)的第一个参数,从流中获得4作为第二个参数(b)。0+4得到4,它成了新的累积值。然后再用累积值和流中下一个元素5调用Lambda,产生新的累积值9。接下来,再用累积值和下一个元素3调用Lambda,得到12。最后,用12和流中最后一个元素9调用Lambda,得到最终结果21
  • Integer的静态sum方法:Integer::sum

    1. public class Test9 {
    2. public static void main(String[] args) {
    3. List<Integer> numbers = Arrays.asList(1, 2, 3, 3, 4, 5, 6, 8);
    4. Integer sum = numbers.stream().reduce(0, Integer::sum);
    5. System.out.println(sum);
    6. }
    7. }
  • 无初始值

  • reduce还有一个重载的变体,它不接受初始值,但是会返回一个Optional对象。 ```java public class Test10 { public static void main(String[] args) {
    1. List<Integer> numbers = Arrays.asList(1, 2, 3, 3, 4, 5, 6, 8);
    2. Optional<Integer> optional = numbers.stream().reduce(Integer::sum);
    } }
  1. - **最大值和最小值**
  2. - 用归约就可以计算最大值和最小值了
  3. ```java
  4. public class Test11 {
  5. public static void main(String[] args) {
  6. List<Integer> numbers = Arrays.asList(1, 2, 3, 3, 4, 5, 6, 8);
  7. Optional<Integer> optionalMax = numbers.stream().reduce(Integer::max);
  8. Optional<Integer> optionalMin = numbers.stream().reduce(Integer::min);
  9. }
  10. }

image.png

  • 怎样用map和reduce方法数一数流中有多少个菜呢?
  • 你可以把流中每个元素都映射成数字1,然后用reduce求和。 ```java public class Test12 { public static void main(String[] args) {
    1. List<Dish> menu = DishClient.getMenu();
    2. Integer reduce = menu.stream().map(d -> 1).reduce(0, (a, b) -> a + b);
    } }
  1. - 归约方法的优势与并行化
  2. - 相比于前面写的逐步迭代求和,使用reduce的好处在于,这里的迭代被内部迭代抽象掉了,这让内部实现得以选择并行执行reduce操作。并行化需要另一种办法:将输入分块,分块求和,最后再合并起来。在下面的章节会学习fork/join框架,看到使用分支/合并框架来做是什么样子。
  3. - 实现并发
  4. ```java
  5. public class Test12 {
  6. public static void main(String[] args) {
  7. List<Dish> menu = DishClient.getMenu();
  8. Integer reduce = menu.parallelStream().map(d -> 1).reduce(0, (a, b) -> a + b);
  9. }
  10. }
  • 但要并行执行这段代码也要付一定代价,我们稍后会向你解释:传递给reduce的Lambda不能更改状态(如实例变量),而且操作必须满足结合律才可以按任意顺序执行。
  • 流操作:无状态和有状态
  • 诸如map或filter等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)
  • 但诸如reduce、sum、max等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个int或double。不管流中有多少元素要处理,内部状态都是有界的
  • 相反,诸如sort或distinct等操作一开始都和filter和map差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。
  • 中间操作和终端操作 | 操作 | 类型 | 返回类型 | 使用的类型/函数式接口 | 函数描述符 | | —- | —- | —- | —- | —- | | filter | 中间 | Stream | Predicate | T -> boolean | | distinct | 中间 | Stream | | | | skip | (有状态-无界)
    中间
    (有状态-有界) | Stream | long | | | limit | (有状态-无界)
    中间
    (有状态-有界) | Stream | long | | | map | 中间 | Stream | Function | T -> R | | flatMap | 中间 | Stream | Function> | T -> Stream | | sorted | 中间
    (有状态-无界) | Stream | Compartor | (T,T) -> int | | anyMatch | 终端 | boolean | Predicate | T -> boolean | | noneMatch | 终端 | boolean | Predicate | T -> boolean | | allMatch | 终端 | boolean | Predicate | T -> boolean | | findAny | 终端 | Optional | | | | findFirst | 终端 | Optional | | | | foreach | 终端 | void | Consumer | T -> void | | collect | 终端 | R | Collector | | | reduce | 终端
    (有状态-有界) | Optional | BinaryOperator | (T,T) -> T | | count | 终端 | long | | |

  • 我们来看一个不同的领域:执行交易的交易员。你的经理让你为八个查询找到答案。

    • 找出2011年发生的所有交易,并按交易额排序(从低到高)
    • 交易员都在哪些不同的城市工作过?
    • 查找所有来自于剑桥的交易员,并按姓名排序。
    • 返回所有交易员的姓名字符串,按字母顺序排序。
    • 有没有交易员是在米兰工作的?
    • 打印生活在剑桥的交易员的所有交易额。
    • 所有交易中,最高的交易额是多少?
    • 找到交易额最小的交易。
  • 领域:交易员和交易

    1. @Data
    2. public class Trader {
    3. private final String name;
    4. private final String city;
    5. public Trader(String name, String city) {
    6. this.name = name;
    7. this.city = city;
    8. }
    9. }
    1. @Data
    2. public class Transaction {
    3. private final Trader trader;
    4. private final int year;
    5. private final int value;
    6. public Transaction(Trader trader, int year, int value) {
    7. this.trader = trader;
    8. this.year = year;
    9. this.value = value;
    10. }
    11. }

    ```java public class OrderClient { public static final List getTransactions() {

    1. Trader raoul = new Trader("Raoul", "Cambridge");
    2. Trader mario = new Trader("Mario", "Milan");
    3. Trader alan = new Trader("Alan", "Cambridge");
    4. Trader brain = new Trader("Brain", "Cambridge");
    5. List<Transaction> transactions = Arrays.asList( //
    6. new Transaction(brain, 2011, 300), //
    7. new Transaction(raoul, 2012, 100), //
    8. new Transaction(raoul, 2011, 400), //
    9. new Transaction(mario, 2012, 710), //
    10. new Transaction(mario, 2012, 700), //
    11. new Transaction(alan, 2012, 950) //
    12. );
    13. return transactions;

    } }

  1. - 找出2011年发生的所有交易,并按交易额排序(从低到高)
  2. ```java
  3. public class Case1 {
  4. // 找出2011年发生的所有交易,并按交易额排序(从低到高)
  5. public static void main(String[] args) {
  6. List<Transaction> transactions = OrderClient.getTransactions();
  7. List<Transaction> transactionList = transactions//
  8. .stream()//
  9. .filter(o -> o.getYear() == 2011)//
  10. .sorted((o1, o2) -> o1.getValue() - o2.getValue())//
  11. .collect(Collectors.toList());
  12. }
  13. }
  • 交易员都在哪些不同的城市工作过? ```java public class Case2 { // 交易员都在哪些不同的城市工作过? public static void main(String[] args) {
    1. List<Transaction> transactions = OrderClient.getTransactions();
    2. List<String> cities = transactions //
    3. .stream() //
    4. .map(transaction -> transaction.getTrader().getCity()) //
    5. .distinct() //
    6. .collect(Collectors.toList()); //
    } }
  1. - 查找所有来自于剑桥的交易员,并按姓名排序。
  2. ```java
  3. public class Case3 {
  4. // 查找所有来自于剑桥的交易员,并按姓名排序。
  5. public static void main(String[] args) {
  6. List<Transaction> transactions = OrderClient.getTransactions();
  7. List<Trader> cambridge = transactions //
  8. .stream() //
  9. .map(Transaction::getTrader)//
  10. .filter(t -> t.getCity().equals("Cambridge")) //
  11. .distinct() //
  12. .sorted(Comparator.comparing(Trader::getName)) //
  13. .collect(Collectors.toList());
  14. }
  15. }
  • 返回所有交易员的姓名字符串,按字母顺序排序。 ```java public class Case4 { // 返回所有交易员的姓名字符串,按字母顺序排序。 public static void main(String[] args) {
    1. List<Transaction> transactions = OrderClient.getTransactions();
    2. String names = transactions //
    3. .stream() //
    4. .map(Transaction::getTrader)//
    5. .map(Trader::getName) //
    6. .distinct() //
    7. .sorted() //
    8. .reduce("", (a, b) -> a + b);
    } }
  1. - 有没有交易员是在米兰工作的?
  2. ```java
  3. public class Case5 {
  4. // 有没有交易员是在米兰工作的?
  5. public static void main(String[] args) {
  6. List<Transaction> transactions = OrderClient.getTransactions();
  7. Optional<Transaction> optional = transactions //
  8. .stream() //
  9. .filter(transaction -> transaction.getTrader().getCity().equals("Milan"))//
  10. .findAny();
  11. if (optional.isPresent()){
  12. System.out.println("有在milan工作的");
  13. }
  14. }
  15. }
  • 打印生活在剑桥的交易员的所有交易额。 ```java public class Case6 { // 打印生活在剑桥的交易员的所有交易额。 public static void main(String[] args) {

    1. List<Transaction> transactions = OrderClient.getTransactions();
    2. List<Integer> cambridge = transactions //
    3. .stream() //
    4. .filter(t -> t.getTrader().getCity().equals("Cambridge"))//
    5. .map(Transaction::getValue)//
    6. .collect(Collectors.toList());

    } }

  1. - 所有交易中,最高的交易额是多少?
  2. ```java
  3. public class Case7 {
  4. // 所有交易中,最高的交易额是多少?
  5. public static void main(String[] args) {
  6. List<Transaction> transactions = OrderClient.getTransactions();
  7. Integer max = transactions //
  8. .stream() //
  9. .map(Transaction::getValue)//
  10. .reduce(0, Integer::max);
  11. }
  12. }
  • 找到交易额最小的交易。 ```java public class Case8 { // 找到交易额最小的交易。 public static void main(String[] args) {
    1. List<Transaction> transactions = OrderClient.getTransactions();
    2. Optional<Transaction> optional = transactions //
    3. .stream() //
    4. .reduce((t1, t2) -> t1.getValue() < t2.getValue() ? t1 : t2);
    } }
  1. ```java
  2. public class Case9 {
  3. // 找到交易额最小的交易。
  4. public static void main(String[] args) {
  5. List<Transaction> transactions = OrderClient.getTransactions();
  6. Optional<Transaction> optional = transactions //
  7. .stream() //
  8. .min(Comparator.comparing(Transaction::getValue));
  9. }
  10. }

数值流

  • 我们在前面看到了可以使用reduce方法计算流中元素的总和。例如,你可以像下面这样计算菜单的热量 ```java public class Test1 { public static void main(String[] args) {
    1. List<Dish> menu = DishClient.getMenu();
    2. Integer reduce = menu.stream().map(Dish::getCalories).reduce(0, Integer::sum);
    } }
  1. - 这段代码的问题是,它有一个暗含的装箱成本。每个Integer都必须拆箱成一个原始类型,再进行求和。
  2. - **原始类型流特化**
  3. - Java 8引入了三个原始类型特化流接口来解决这个问题:IntStreamDoubleStreamLongStream,分别将流中的元素特化为intlongdouble,从而避免了暗含的装箱成本。
  4. - **映射到数值流**
  5. - 将流转换为特化版本的常用方法是mapToIntmapToDoublemapToLong。这些方法和前面说的map方法的工作方式一样,只是它们返回的是一个特化流。你可以像下面这样用mapToIntmenu中的卡路里求和。
  6. - 请注意,如果流是空的,sum默认返回0IntStream还支持其他的方便方法,如maxminaverage等。
  7. ```java
  8. public class Test2 {
  9. public static void main(String[] args) {
  10. List<Dish> menu = DishClient.getMenu();
  11. int reduce = menu.stream().mapToInt(Dish::getCalories).reduce(0, Integer::sum);
  12. }
  13. }
  • 转换回对象流
  • 同样的,一旦有了数值流,可能会想转化为非特化流。例如,IntStream上的操作只能产生原始整数:IntStream的map操作接受的Lambda必须接受int并返回int(一个IntUnaryOperator),但是你可能想要生成另一类值,比如Dish。为此,你需要访问Stream接口中定义的那些更广义的操作。要把原始流转换成一般流(每个int都会装箱成一个Integer),可以使用boxed方法。 ```java public class Test3 { public static void main(String[] args) {
    1. List<Dish> menu = DishClient.getMenu();
    2. IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
    3. Stream<Integer> boxed = intStream.boxed();
    } }
  1. - **默认值OptionalInt**
  2. - 如果你要计算IntStream中的最大元素,就得换个法子了,因为0是错误的结果。如何区分没有元素的流和最大值真的是0的流呢?前面我们介绍了Optional类,这是一个可以表示值存在或不存在的容器。Optional可以用IntegerString等参考类型来参数化。对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalIntOptionalDoubleOptionalLong
  3. ```java
  4. public class Test4 {
  5. public static void main(String[] args) {
  6. List<Dish> menu = DishClient.getMenu();
  7. OptionalInt max = menu.stream().mapToInt(Dish::getCalories).max();
  8. int maxNum = max.orElse(1);
  9. }
  10. }
  • 数值范围:Java 8引入了两个可以用于IntStream和LongStream的静态方法,帮助生成这种范围:range和rangeClosed。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range是不包含结束值的,而rangeClosed则包含结束值。

    1. public class Test5 {
    2. public static void main(String[] args) {
    3. IntStream intStream = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);
    4. System.out.println(intStream.count());
    5. }
    6. }
    7. // 50
    1. public class Test6 {
    2. public static void main(String[] args) {
    3. IntStream intStream = IntStream.range(1, 100).filter(n -> n % 2 == 0);
    4. System.out.println(intStream.count());
    5. }
    6. }
    7. // 49
  • 数值流应用:勾股数,创建一个勾股数流

  • 表示三元数:使用具有三个元素的int数组,比如new int[]{3, 4,5},来表示勾股数(3, 4, 5)
  • 筛选成立的组合:假定有人为你提供了三元数中的前两个数字:a和b。怎么知道它是否能形成一组勾股数呢?需要测试a * a+b * b的平方根是不是整数

    1. filter(b -> Math.sqrt(a*a + b*b) % 1 == 0)
    2. // Math.sqrt(a * a+b * b) 是判断 返回的结果是不是整数的方法
  • 生成三元组

  • 在筛选之后,你知道a和b能够组成一个正确的组合。现在需要创建一个三元组。你可以使用map操作,像下面这样把每个元素转换成一个勾股数组

    1. stream.filter(b -> Math.sqrt(a * a + b * b) % 1 == 0).map(b -> new int[] { a, b, (int) Math.sqrt(a * a + b * b) });
  • 生成b值

    1. IntStream.rangeClosed(1, 100).filter(b -> Math.sqrt(a * a + b * b) % 1 == 0).boxed().map(b -> new int[] { a, b, (int) Math.sqrt(a * a + b * b) });
  • 请注意,你在filter之后调用boxed,从rangeClosed返回的IntStream生成一个Stream。这是因为你的map会为流中的每个元素返回一个int数组。而IntStream中的map方法只能为流中的每个元素返回另一个int,这可不是你想要的!你可以用IntStream的mapToObj方法改写它,这个方法会返回一个对象值流.

    1. IntStream.rangeClosed(1, 100).filter(b -> Math.sqrt(a * a + b * b) % 1 == 0).mapToObj(b -> new int[] { a, b, (int) Math.sqrt(a * a + b * b) });
  • 生成值

  • 这里有一个关键的假设:给出了a的值。现在,只要已知a的值,你就有了一个可以生成勾股数的流。如何解决这个问题呢?就像b一样,你需要为a生成数值! ```java public class Test7 { public static void main(String[] args) {
    1. Stream<int[]> stream = IntStream.rangeClosed(1, 100).boxed() //
    2. .flatMap(a -> IntStream //
    3. .rangeClosed(a, 100)//
    4. .filter(b -> Math.sqrt(a * a + b * b) % 1 == 0)//
    5. .mapToObj(b -> new int[] { a, b, (int) Math.sqrt(a * a + b * b) }));
    } }
  1. - **优化**
  2. ```java
  3. public class Test8 {
  4. public static void main(String[] args) {
  5. Stream<int[]> stream = IntStream.rangeClosed(1, 100).boxed() //
  6. .flatMap(a -> IntStream //
  7. .rangeClosed(a, 100)//
  8. .mapToObj(b -> new int[] { a, b, (int) Math.sqrt(a * a + b * b) })//
  9. .filter(t -> t[2] % 1 == 0));
  10. }
  11. }

从多个源创建流

  • 由值创建流:可以使用静态方法Stream.of,通过显式值创建一个流 ```java public class Test9 { public static void main(String[] args) {
    1. Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6);
    } }
  1. - 空流
  2. ```java
  3. public class Test10 {
  4. public static void main(String[] args) {
  5. Stream<String> integerStream = Stream.empty();
  6. }
  7. }
  • 从数组创建流 ```java public class Test11 { public static void main(String[] args) {
    1. int[] numbers = { 1, 2, 3, 4, 5, 566, 88 };
    2. IntStream stream = Arrays.stream(numbers);
    } }
  1. - 从文件创建流
  2. ```java
  3. public class Test12 {
  4. public static void main(String[] args) {
  5. long lineWords = 0;
  6. try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
  7. lineWords = lines.flatMap(line -> Arrays.stream(line.split(""))).distinct().count();
  8. } catch (Exception e) {
  9. }
  10. }
  11. }

无限流

  • 创建无限流
  • Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由iterate和generate产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用limit(n)来对这种流加以限制,以避免打印无穷多个值。
  • 迭代 ```java public class Test13 { public static void main(String[] args) {
    1. Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
    } }
  1. - iterate方法接受一个初始值(在这里是0),还有一个依次应用在每个产生的新值上的LambdaUnaryOperator<t>类型)。这里,我们使用Lambda n-> n+2,返回的是前一个元素加上2。因此,iterate方法生成了一个所有正偶数的流:流的第一个元素是初始值0。然后加上2来生成新的值2,再加上2来得到新的值4,以此类推。这种iterate操作基本上是顺序的,因为结果取决于前一次应用。请注意,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。正如我们前面所讨论的,这是流和集合之间的一个关键区别。我们使用limit方法来显式限制流的大小。这里只选择了前10个偶数。然后可以调用forEach终端操作来消费流,并分别打印每个元素。
  2. - **计算有限个斐波那契数列**
  3. ```java
  4. public class Test14 {
  5. public static void main(String[] args) {
  6. Stream.iterate(new int[] { 0, 1 }, t -> new int[] { t[1], t[0] + t[1] }).limit(20).forEach(t -> System.out.println("(" + t[0] + " " + t[1] + ")"));
  7. }
  8. }
  • 生成
  • 与iterate方法类似,generate方法也可让你按需生成一个无限流。但generate不是依次对每个新生成的值应用函数的。它接受一个Supplier类型的Lambda提供新的值。 ```java public class Test15 { public static void main(String[] args) {
    1. Stream.generate(Math::random).limit(5).forEach(System.out::println);
    } }

```

小结

  • Streams API可以表达复杂的数据处理查询。常用的流操作总结在表中。
  • 可以使用filter、distinct、skip和limit对流做筛选和切片。
  • 可以使用map和flatMap提取或转换流中的元素。
  • 可以使用findFirst和findAny方法查找流中的元素。你可以用allMatch、noneMatch和anyMatch方法让流匹配给定的谓词。这些方法都利用了短路:找到结果就立即停止计算;没有必要处理整个流。
  • 你可以利用reduce方法将流中所有的元素迭代合并成一个结果,例如求和或查找最大元素。
  • filter和map等操作是无状态的,它们并不存储任何状态。reduce等操作要存储状态才能计算出一个值。sorted和distinct等操作也要存储状态,因为它们需要把流中的所有元素缓存起来才能返回一个新的流。这种操作称为有状态操作。
  • 流有三种基本的原始类型特化:IntStream、DoubleStream和LongStream。它们的操作也有相应的特化。
  • 流不仅可以从集合创建,也可从值、数组、文件以及iterate与generate等特定方法创建。
  • 无限流是没有固定大小的流。

    参考文章

  • 《Java 8 in Action》

  • 《Java8函数式编程》