Java8-使用流

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java8-使用流相关的知识,希望对你有一定的参考价值。

在之前,你已经看到了流让你从外部迭代转向内部迭代。这样,你就用不着写下面这样的代码来显示地管理数据集合的迭代(外部迭代)了:

1         List<Dish> vegetarianDishes = new ArrayList<>();
2         for (Dish d : menu) {
3             if (d.isVegetarion()) {
4                 vegetarianDishes.add(d);
5             }
6         }

你可以使用支持filter和collect操作的Stream API(内部迭代)管理对集合数据的迭代。你只需要将筛选行为作为行为参数传递给filter方法就行了。

1         List<Dish> vegetarianDishes = 
2                 menu.stream().
3                 filter(Dish::isVegetarion).
4                 collect(Collectors.toList());

这种处理数据的方式很有用,因为你让Stream API管理如何处理数据。这样Stream API就可以在背后进行多种优化。此外,使用内部迭代的话,Stream API可以决定并行运行你的代码。这要是用外部迭代的话就办不到了,因为你只能用单一线程挨个迭代。

1.筛选和切片

  在接下来的内容,我们来看啊看如何选择流中的元素:用谓词筛选,筛选出各不相同的元素,忽略流中的头几个元素,或将流截断至指定长度。

  (1).用谓词筛选

 Streams接口支持filter方法(你现在应该很熟悉了吧)。该操作会接收一个谓词(一个返回boolean的函数)作为参数,并且返回一个包括所有谓词的元素的流。例如,你可以像下图所示这样的,筛选出所有的素材,创建一张素食菜单:

  

1         List<Dish> vegetarianMenu = 
2                 menu.stream().
3                 filter(Dish::isVegetarion).//方法引用检查菜肴是否适合素食者
4                 collect(Collectors.toList());

技术分享

  (2).筛选各异的元素

 流还支持一个叫作distinct方法,它会返回一个元素各异(根据流所生成元素的hashCode和equals方法实现)的流。例如,一下代码会筛选出列表中所有额偶数,并且确保没有重复。下图直观地显示了这个过程。

1         List<Integer> numbers = Arrays.asList(1,2, 1,2, 3, 3, 2, 4);
2         numbers.stream().
3         filter(i -> i % 2 == 0).
4         distinct().
5         forEach(System.out::println);

技术分享

  (3).截断流

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

  

1         List<Dish> dishes = 
2                 menu.stream().
3                 filter(d -> d.getCalories() > 300).
4                 limit(3).
5                 collect(Collectors.toList());

  下图展示了filter和limit的组合。你可以看到,该方法只选出了符合谓词的头三个元素,然后就立即返回了结果。

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

  技术分享

  (4).跳过元素

  流还支持skip(n)方法,返回一个扔掉前n个元素的流。如果流中元素个数不足n个,则返回一个一个空流。请注意,limit(n)和skip(n)是互补的!例如,下面的代码将跳过300卡路里的头两道菜,并返回剩下的。下图展示了这个查询。

1         List<Dish> dishes = 
2                 menu.stream().
3                 filter(d -> d.getCalories() > 300).
4                 skip(2).
5                 collect(Collectors.toList());

技术分享

2.映射

  一个非常常见的数据处理就是从某些对象中选择信息。比如在sql里,你可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具

  (1).对流中每一个元素应用函数

  流还支持map方法,它会接收一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一次,是因为它和转换类似,但其中的细微差别在于它是"创建一个新版本"而不是去“修改”)。例如,下面的代码把方法引用Dish::getName传给了map方法,来提取流中的菜肴的名称:

1         List<String> dishNames = 
2                 menu.stream().
3                 map(Dish::getName).
4                 collect(Collectors.toList());

  因此getName方法返回一个String,所以map方法输出的流的类型就是Stream<String>。

  让我们看一个稍微不同的例子来巩固一下对map的理解。给定一个单词列表,你想要利用返回另一个列表,显示每个单词中有几个字母。怎么做呢?你需要对列表中的每个元素应用一个函数。这听起来正好该用map方法去做!应用的函数应该接受一个单词,并且返回一个其长度。你可以像下面这样,给map传递一个方法引用String::length来解决问题:

1         List<String> words = Arrays.asList("Java 8", "Lambda", "android");
2         List<Integer> wordLengths = words.stream().map(String::length).collect(Collectors.toList());

  现在让我们回到提取菜名的例子。如果你要找出每道菜的名称有多长,怎么做?你可以像下面这样:

1         List<Integer> dishNames = 
2                 menu.stream().
3                 map(Dish::getName).
4                 map(String::length).
5                 collect(Collectors.toList());

(2).流的扁平化

  你已经看到如何使用map方法返回列表中每个单词的长度了。让我们拓展一下,对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表["Hello", "world"],你想要返回列表["H", "e", "l", "o", " W", "r", "d"]。

  你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用distinct来过滤重复的字符。第一个版本可能是是这样的:

 1         List<String> words = Arrays.asList("Hello", "World");
 2         List<String[]> list = 
 3                 words.stream().
 4                 map(w -> w.split("")).
 5                 distinct().
 6                 collect(Collectors.toList());
 7         for(int i = 0, n = list.size(); i < n; i++)
 8         {
 9             String[] strings = list.get(i);
10             for(int j = 0, m = strings.length; j < m; j++){
11                 System.out.println(strings[j]);
12             }
13         }

  这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[](String类型的数组)。因此,map返回的流实际上是Stream<String[]>类型的。你真正想要的是用Stream<String>来表达式一个字符串,下图说明了这个问题:

技术分享

幸好可以用flagMap来解决这个问题!让我们一步步看看怎么解决它。

 A.尝试使用map和Arrays.stream()

 首先,你需要一个字符流,而不是数组流。有一个叫作Arrays.stream()的方法可以接收一个数组并且产生一个流,例如:

 

1         String [] arrayOfWords = {"Goodbye", "World"};
2         Stream<String> streamOfWords = Arrays.stream(arrayOfWords);

把用在前面的那个流水线中,看看会发生什么:

1 List<String> words = Arrays.asList("Goodbye", "World");
2         List<Stream<String>> list = 
3                 words.stream().
4                 map(word -> word.split("")). //將String转换为String[]
5                 map(Arrays::stream). //将String[]每个元素都转换为流,返回的是Stream<String>,但是有多个
6                 distinct(). //去重
7                 collect(Collectors.toList()); // 将多个Stream<String>的对象转换为List对象,因此为List<Stream<String>>

 

当前的解决方案仍然搞不定!这是因为,你现在得到的是一个流的集合(更准确的说,是Stream<String>的集合)!的确,你先是把每个单词转换成为一个字母数组,然后把数组的每个元素变成了一个独立的流。

  B.使用flatMap

  你可以使用像下面这样使用flatMap来解决这个问题:

1           List<String> list2 = 
2                   words.stream(). //从word集合获得Stream<String>对象
3                   map(word -> word.split("")). //将Stream<String>流对象中的String通过map方法映射成为String[],返回Stream<String[]>
4                   flatMap(Arrays::stream). // Array::stream将Stream<String[]>里面的String[]转换为String,
5                                           //获得Stream<String>对象,但是这个对象有多个,
6                                           //flatMap方法将多个Stream<String>转换为一个Stream<String>
7                   distinct().
8                   collect(Collectors.toList());

  使用flatMap方法的效果是,各个数组并不是分别映射成为一个流,而是映射成为流的内容。所有使用map(Arrays::stream)时生成单个流都被合并起来,即扁平化为一个流。下图说明了使用flatMap方法的效果。

技术分享

  总之,flagMap方法让你把每一个流中的每个值都换成另一个流,然后把所有的流连接起来成为另一个流。

  (3).看不太懂的例子

  A.给定一个数字列表,如何返回一个由每个数字的平方构成的列表呢?例如,给定[1,2,3,4],应该返回[1, 4, 9, 16]

1         List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
2         List<Integer> squares = 
3                 numbers.stream().
4                 map(n -> n * n).
5                 collect(Collectors.toList());

 B.给定两个数字列表,如何返回所有的数对呢?例如,给定里列表[1,2,3]和列表[3,4],应该返回[(1,3), (1,4), (2, 3), (3,3), (3,4)].为了简单起见,你可以用欧两个元素的数组来代表数对。

1         List<Integer> numbers1 = Arrays.asList(1, 2, 3);
2         List<Integer> numbers2 = Arrays.asList(3, 4);
3         List<int[]> pairs = 
4                 numbers1.stream().// 将numbers集合转换为Stream<Integer>
5                 flatMap(i -> numbers2.stream(). //将numbers2集合转换为Stream<Integer>
6                         map(j -> new int[]{i, j}) //使用map映射,将Integer映射成为int[],返回Stream<int[]>
7                         ). //由于numbers1的流对象里面有多个元素,所以与numbers2匹配,会产生多个Stream<int[]>
8                            //使用flatMap将多个Stream<int[]>合并成为一个Stream<int>
9                 collect(Collectors.toList());

  C.如何扩展前一个例子,只返回总和能被3整除的整对。例如(2, 4)和(3, 3)是可以的。

 1         List<Integer> numbers1 = Arrays.asList(1, 2, 3);
 2         List<Integer> numbers2 = Arrays.asList(3, 4);
 3         List<int[]> pairs = 
 4                 numbers1.stream().// 将numbers集合转换为Stream<Integer>
 5                 flatMap(i -> numbers2.stream() //将numbers2集合转换为Stream<Integer>
 6                         .filter(j -> (i + j) % 3 == 0) //过滤
 7                         .map(j -> new int[]{i, j}) //使用map映射,将Integer映射成为int[],返回Stream<int[]>
 8                         ). //由于numbers1的流对象里面有多个元素,所以与numbers2匹配,会产生多个Stream<int[]>
 9                            //使用flatMap将多个Stream<int[]>合并成为一个Stream<int>
10                 collect(Collectors.toList());

3.查找和映射

  另一个常见的数据处理是看看数据集中的某些元素是否匹配一个给定的属性。Stream API通过allMatch、anyMatch、noneMatch、findMatch和findAny方法提供这样的工具。

  (1).检查谓词是否至少匹配一个元素

   anyMatch方法可以回答:“流中是否有一个元素能够给定的谓词”。比如,你可以使用它来看看菜单里面里面是否含有素食可选择:

1         if (menu.stream().anyMatch(Dish::isVegetarion)) {
2             System.out.println("The menu is (somewhat) vegetarian frinendly!");
3         }

 anyMatch方法返回的是一个boolean类型,因此是一个终端操作。

  (2).检查谓词是否匹配所有元素

  allMatch方法的工作原理和anyMatch类似,但是它会看看流中的元素是否都能匹配给定的谓词。比如,你可以用填空你看菜品是否有利于健康(即所有菜的热量都低于1000卡路里):

1 boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);

 和allMatch相对的是noneMatch。它可以确保流中没有任何元素与给定的谓词匹配。比如,你可以用noneMatch重写前面的例子:

1 boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);

anyMatch、allMatch和noneMatch这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中的&&和||运算符短路在流中的版本。

  短路求值

 有些操作不需要处理整个流的就能得到结果。例如,假设你需要对一个用and连接起来的大布尔表达式求值。不管表达有多长,你只需要找到一个表达式为false,,你就可以推断出整个表达式将返回false,所以用不着计算整个表达式。这就是短路。

 对于流而言,某些操作(例如:allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果来了。同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流。

  (3).查找元素

  findAny元素方法将返回当前流中的任意元素。它可以与其他刘操作结合使用。比如,你可能想找到移到素食菜肴。你可以结合使用filter和findAndy方法来实现这个查询:

  

1         Optional<Dish> dish = 
2                 menu.stream()
3                 .filter(Dish::isVegetarion)
4                 .findAny();

流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。不过慢着,代码里面Optional是个什么玩意儿呢?

  Optional简介

  Optional<T>类(java.util.Optional)是一个容器类,代表一个值存在或者不存在。在上面的代码中,finAny可能什么元素都没有找到。Java8的库设计人员引入了Optional<T>,这样就不用返回众所周知容易出问题的null了,我们在这里不会详细的讨论Optional,因为之后会详细解释你的代码如何利用Opiontal,避免和null检查相关的bug。不过现在,了解一下Optional里面几种可以迫使你现实地检查或者处理值不存在的情形的方法也不错。

  A.isPresent() --将在Optional包含值的时候返回true,否则返回false。

  B.ifPresent(Consumer<T> block) -- 会在值存在的时候执行给定的代码。我们在之前介绍了Consumer函数式接口;它让你传递一个接收T类型参数,并且返回void的Lambda表达式。

  C. T get() -- 会在值存在时返回值,否则会抛出一个NoSuchElement异常。

  D.orElse(T other) --  会在值存在时返回值,否则返回一个默认值。

  例如,在前面的代码中你需要显示地检查Optional对象总是否存在一道菜可以访问其名称:

1             menu.stream()
2                 .filter(Dish::isVegetarion)
3                 .findAny() //返回一个Optional<Dish>对象
4                 .ifPresent(d -> System.out.println(d.getName())); //如果包含一个值就打印它,否则什么都不做。

  (4).查找第一个元素

   有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由List或者排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个findFirst方法,它的工作方式类似于findAny。例如,给定一个数字列表,下面的代码能找出第一个平方能被3整除的数: 

1         List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
2         Optional<Integer> firstSquareDivisibleByThree = 
3                 someNumbers.stream()
4                            .map(x -> x *x)
5                            .filter(x -> x % 3== 0)
6                            .findFirst(); // 9

  何时使用findFirst和findAny

  你可能会想,为什么会同时有findFirst和findAny呢?答案是并行的。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用findAny,因为它在使用并行流上限制较少。

4.归约

 到目前为止,你见到过的终端操作都是返回一个boolean(allMatch之类的)、void(forEach)或者Optional对象(findAny等)。你也见过了使用collect来将流中的所有元素组合成一个List。

  在这里,你将看到如何把一个流中的元素组合起来,使用reduce操作来表达更加复杂的查询。比如“计算菜单中的总卡路里”或者“菜单中卡路里最高的菜是哪一个”。此类查询需要将流中所有元素反复结合起来,得到一个值,比如一个Integer。这样的查询可以被归类为归约操作(将流归约成为一个值)。用函数式编程语言的术语来说,这就是折叠(fold),因为你可以将这个操作看成把一张长长的纸(你的流)反复折叠成一个方块,而这就是折叠操作的结果。

  (1).元素求和

  在我们要就reduce方法之前,先来看看如何使用for-each循环对数字列表中的元素求和:

  

1         List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
2         int sum = 0;
3         for (int i : someNumbers) {
4             sum += i;
5         }

someNumbers中的每个元素都用加法运算符反复的迭代来得到结果。通过反复使用加法,你把一个数字列表归约成一个数字。这段代码中有两个参数:

  A.总和变量的初始值,在这里是0;

  B.将列表所有元素结合在一起的操作,这里是+。

  要是还能把所有的数字相乘,而不必去复制粘贴这段代码,岂不是很好?这正是reduce操作的用武之地,它对这种重复应用的模式做了抽象。你可以像下面这样对流中所有的元素求和。

1 int sum = someNumbers.stream().reduce(0, (a, b) -> a + b);

reduce接收两个参宿:

  A.一个初始值,这里是0;

  B.一个是BinaryOperator<T>来将两个元素结合起来产生一个新值,这里我们用的是Lambda表达式 (a,b) -> a + a。

  你也很容易把所有的元素相乘,只需要将另一个Lambda:(a, b) -> a * b传递给reduce操作就可以了:

1 int produt = someNumbers.stream().reduce(0, (a, b) -> a * b);

下图展示了reduce操作是如何作用于一个流的:Lambda反复结合每个元素,直到流被归约成一个值。

  技术分享

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

你可以使用方法引用让这段代码更加的简洁。在java8中,Integer类现有一个静态的sum方法来对两个参数求和,这恰好是我们想要的, 用不着反复用Lambda表达式写同一段代码:

1 int produt = someNumbers.stream().reduce(0, Integer::sum);

  无初始值:

  reduce还有一个重载的变体,它不接收初始值,但是会返回一个Optional对象:

1 Optional<Integer> optional = someNumbers.stream().reduce(Integer::sum);

为什么它会返回一个Optional<Integer>呢?考虑流中没有任何元素的情况。reduce操作无法返回其和,因为它没有初始值。这就是为什么结果被包括在一个Optional对象中,以表明可能不存在。现在看看用reduce还能做什么。

  (2).最大值和最小值

  原来,只要用归约就可以计算最大值和最小值了!让我们来看看如何利用刚刚学到的reduce来计算流中最大或者最小的元素。正如你前面看到的,reduce接收两个参数:

  A.一个初始值

  B. 一个Lambda来把两个流元素结合在一起来并且产生一个新值。

 Lambda是一步步用加法运算符应用到流中每个元素上的,如下图所示。因此你需要一个给定两个元素能够返回最大值的Lambda。reduce操作会考虑新值和流中下一个元素。并产生一个新的最大值,直到整个流程消耗完了。你可以像下面这样使用reduce来计算流中的最大值,如图所示。

  

1 Optional<Integer> max = someNumbers.stream().reduce(Integer::max);

 

以上是关于Java8-使用流的主要内容,如果未能解决你的问题,请参考以下文章

Java8-使用流

Java开发工程师进阶篇-Java8的Stream流使用技巧

java8 流操作

Java8 Stream流方法

Java8实战使用并行流

Java8 Stream流如何操作集合,一文带你了解!