一. 介绍

Stream是 Java 8新增加的类,用来补充集合类。
Stream代表数据流,流中的数据元素的数量可能是有限的,也可能是无限的。
关于流和其它集合具体的区别,可以参照下面的列表:

  1. 不存储数据。流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。
  2. 函数式编程。流的操作不会修改数据源,例如filter不会将数据源中的数据删除。
  3. 延迟操作。流的很多操作如filter,map等中间操作是延迟执行的,只有到终点操作才会将操作顺序执行。
  4. 可以解绑。对于无限数量的流,有些操作是可以在有限的时间完成的,比如limit(n)findFirst(),这些操作可是实现”短路”(Short-circuiting),访问到有限的元素后就可以返回。
  5. 纯消费。流的元素只能访问一次,类似Iterator,操作没有回头路,如果你想从头重新访问流的元素,对不起,你得重新生成一个新的流。

    我们为什么需要 Stream API

    Stream 作为 Java 8 的一大亮点,它与 java.io 包里的 InputStream 和 OutputStream 是完全不同的概念。
    流的操作是以管道的方式串起来的。流管道包含一个数据源,接着包含零到N个中间操作,最后以一个终点操作结束。

    集合讲的是数据,流讲的是计算

Java Stream流 详解 - 图1
Java 8 中的 Stream 是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。Stream API 借助于同样新出现的 Lambda 表达式,极大的提高编程效率和程序可读性。

同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。所以说,Java 8 中首次出现的 java.util.stream 是一个函数式语言+多核时代综合影响的产物。

浅谈聚合操作(Stream API能协助解决)

在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合函数来完成诸如:

  • 客户每月平均消费金额
  • 最昂贵的在售商品
  • 取十个数据样本作为首页推荐

但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。

这个时候,如果没有Java8提供的Stream API,那简直就是噩梦。在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快。

对Stream进一步理解

简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用(side effect)。(副作用不是return,而是流执行过程中对入参的修改,或者文件的输出,或者println())

Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。

对于 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
Stream 的另外一大特点是,数据源本身可以是无限的(即无限流)

对流的操作概述

流的操作类型分为两种:
Intermediate(中间操作):一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。

map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered

Terminal(终止操作):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。

forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator

还有一种操作被称为 short-circuiting(短路操作)。用以指:

  • 对于一个 intermediate 操作,如果它接受的是一个无限流,它可以返回一个有限的新 Stream。
  • 对于一个 terminal 操作,如果它接受的是一个无限流,但能在有限的时间计算出结果。

    anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit

Stream流的所有操作函数

  • 中间操作(intermediate operation)
    • 无状态(Stateless)
      • unordered()
      • filter()
      • map()
      • mapToInt()
      • mapToLong()
      • mapToDouble
      • flatMap()
      • flatMapToInt()
      • flatMapToLong()
      • flatMapToDouble()
      • peek()
    • 有状态(Stateful)
      • distinct()
      • sorted()
      • limit()
      • skip()
  • 结束操作(terminal operation)
    • 非短路操作
      • forEach()
      • forEachOrdered()
      • toArray()
      • reduce()
      • collect()
      • max()
      • min()
      • count()
    • 短路操作(short-circuiting)
      • anyMatch()
      • allMatch()
      • noneMatch()
      • findFirst()
      • findAny()

        IntStream、LongStream、DoubleStream

        IntStream、LongStream、DoubleStream,java特别为这三种基本数值型提供了对应的 Stream。

        Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。

数值流的构造:

  1. IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
  2. IntStream.range(1, 3).forEach(System.out::println);
  3. IntStream.rangeClosed(1, 3).forEach(System.out::println);

range(),需要传入开始节点和结束节点两个参数,返回的是一个有序的LongStream。包含开始节点和结束节点两个参数之间所有的参数,间隔为1.
rangeClosed的功能和range类似。差别就是rangeClosed包含最后的结束节点,range不包含。

二. 流的特性

1. 并行 Parallelism

所有的流操作都可以串行执行或者并行执行。
除非显示地创建并行流,否则Java库中创建的都是串行流。 Collection.stream()为集合创建串行流而Collection.parallelStream()为集合创建并行流。IntStream.range(int, int)创建的是串行流。通过parallel()方法可以将串行流转换成并行流,sequential()方法将流转换成串行流。
除非方法的Javadoc中指明了方法在并行执行的时候结果是不确定(比如findAny、forEach),否则串行和并行执行的结果应该是一样的。

2. 不能干扰 Non-interference

流可以从非线程安全的集合中创建,当流的管道执行的时候,非concurrent数据源不应该被改变。下面的代码会抛出java.util.ConcurrentModificationException异常:

  1. List<String> l = new ArrayList(Arrays.asList("one", "two"));
  2. Stream<String> sl = l.stream();
  3. sl.forEach(s -> l.add("three"));

在设置中间操作的时候,可以更改数据源,只有在执行终点操作的时候,才有可能出现并发问题(抛出异常,或者不期望的结果),比如下面的代码不会抛出异常:

  1. List l = new ArrayList(Arrays.asList("one", "two"));
  2. Stream sl = l.stream();
  3. l.add("three");
  4. sl.forEach(System.out::println);

对于concurrent数据源,不会有这样的问题,比如下面的代码很正常:

  1. List l = new CopyOnWriteArrayList<>(Arrays.asList("one", "two"));
  2. Stream sl = l.stream();
  3. sl.forEach(s -> l.add("three"));

虽然我们上面例子是在终点操作中对非并发数据源进行修改,但是非并发数据源也可能在其它线程中修改,同样会有并发问题。

3. 无状态 Stateless behaviors

大部分流的操作的参数都是函数式接口,可以使用Lambda表达式实现。它们用来描述用户的行为,称之为行为参数(behavioral parameters)。
如果这些行为参数有状态,则流的操作的结果可能是不确定的,比如下面的代码:

  1. List<String> l = new ArrayList(Arrays.asList("one", "two", ……));
  2. class State {
  3. boolean s;
  4. }
  5. final State state = new State();
  6. Stream<String> sl = l.stream().map(e -> {
  7. if (state.s)
  8. return "OK";
  9. else {
  10. state.s = true;
  11. return e;
  12. }
  13. });
  14. sl.forEach(System.out::println);

上面的代码在并行执行时多次的执行结果可能是不同的。这是因为这个lambda表达式是有状态的。

4. 副作用 Side-effects

流水线上所有操作都执行后,用户所需要的结果(如果有)在哪里?首先要说明的是不是所有的Stream结束操作都需要返回结果,有些操作只是为了使用其副作用(Side-effects),比如使用Stream.forEach()方法将结果打印出来就是常见的使用副作用的场景
有副作用的行为参数是被不鼓励使用的,事实上,除了打印之外其他场景都应避免使用副作用。也许你会觉得在Stream.forEach()里进行元素收集是个不错的选择,就像下面代码中那样,但遗憾的是这样使用的正确性和效率都无法保证,因为Stream可能会并行执行。大多数使用副作用的地方都可以使用归约操作.md)更安全和有效的完成。
很多有副作用的行为参数可以被转换成无副作用的实现

  1. ArrayList<String> list = Lists.newArrayList();
  2. for (int i = 0;i<1000;i++) {
  3. list.add(i+"");
  4. }
  5. ArrayList<String> list2 = Lists.newArrayList();
  6. // 副作用代码
  7. list.parallelStream().forEach(s -> list2.add(s));
  8. System.out.println(list2);

上面的代码结果为,明显在多线程状态下对ArrayList操作发生了错误,同时如果不指定list2的大小,list在扩容时还可能会报ArrayIndexOutOfBoundsException下标越界异常。
image.png

可以改成以下无副作用的代码,或者改用并发集合类CopyOnWriteArrayList

  1. ArrayList<String> list = Lists.newArrayList();
  2. for (int i = 0;i<1000;i++) {
  3. list.add(i+"");
  4. }
  5. List<String> list2 = list.parallelStream().collect(Collectors.toList());
  6. System.out.println(list2);

5. 排序 Ordering

某些流的返回的元素是有确定顺序的,我们称之为 encounter order。这个顺序是流提供它的元素的顺序,比如数组的encounter order是它的元素的排序顺序,List是它的迭代顺序(iteration order),对于HashSet,它本身就没有encounter order。
一个流是否是encounter order主要依赖数据源和它的中间操作,比如数据源List和Array上创建的流是有序的(ordered),但是在HashSet创建的流不是有序的。
sorted()方法可以将流转换成encounter order的,unordered可以将流转换成encounter order的。
注意,这个方法并不是对元素进行排序或者打散,而是返回一个是否encounter order的流。
除此之外,一个操作可能会影响流的有序,比如map方法,它会用不同的值甚至类型替换流中的元素,所以输入元素的有序性已经变得没有意义了,但是对于filter方法来说,它只是丢弃掉一些值而已,输入元素的有序性还是保障的。
对于串行流,流有序与否不会影响其性能,只是会影响确定性(determinism),无序流在多次执行的时候结果可能是不一样的。
对于并行流,去掉有序这个约束可能会提供性能,比如distinctgroupingBy这些聚合操作。

6. 结合性 Associativity

一个操作或者函数op满足结合性意味着它满足下面的条件:

  1. (a op b) op c == a op (b op c)

对于并发流来说,如果操作满足结合性,我们就可以并行计算:

  1. a op b op c op d == (a op b) op (c op d)

比如minmax以及字符串连接都是满足结合性的。

二. 创建Stream

可以通过多种方式创建流:

由集合创建流

Java8 中的 Collection 接口被扩展,提供两个获取流的方法 :

  • Stream stream() : 返回一个顺序流
  • Stream parallelStream()** **: 返回一个并行流
    1. new ArrayList<>().stream();

    由数组创建流

    Java8 中的 Arrays 的静态方法 stream() 可以获取数组流 :static Stream **stream(T[] array)** : 返回一个流重载形式,能够处理对应基本类型的数组IntStream/LongStream/DoubleStream :
  1. Arrays.stream(new int[]{1,2,3})

由值创建流

可以使用静态方法 Stream.of(), 通过显示值创建一个流,它可以接收任意数量的参数:public static Stream of(**T… values**) : 返回一个流

  1. Stream<Integer> integerStream = Stream.of(1);
  2. Stream<String> stringStream = Stream.of("1");

由方法创建流 : 创建无限流

可以使用静态方法 Stream.iterate() 和 Stream.generate(), 创建无限流
迭代流 : public static Stream iterate(final T seed, final UnaryOperator f)

  1. //初值为1的无限等比数列
  2. Stream.iterate(1, n -> n * 2);

生成流 : public static Stream generate(Supplier s)

  1. //无限随机数流
  2. Stream.generate(Math::random)

使用IntStream、LongStream、DoubleStream的static方法创建有限流

  1. IntStream.of(new int[]{1, 2, 3});
  2. IntStream.range(1, 3);
  3. IntStream.rangeClosed(1, 3);

使用随机数类的ints()方法创建无限数值流

  1. Random random = new Random();
  2. IntStream ints = random.ints();

从文件中获得流

使用BufferedReader的lines方法从文件中获得行的流

  1. BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("file.txt")));
  2. Stream<String> lines = bufferedReader.lines();

Files类的操作路径的方法,如listfindwalk等。

其他类提供的创建流

BitSet数值流

  1. IntStream stream = new BitSet().stream();

Pattern 将字符串分隔成流

  1. Pattern pattern = compile(",");
  2. Stream<String> stringStream = pattern.splitAsStream("a,b,c,d");
  3. stringStream.forEach(System.out::println);

JarFile 读取jar文件流

  1. Stream<JarEntry> stream = new JarFile("").stream();

更底层的使用StreamSupport,它提供了将Spliterator转换成流的方法,目前还不了解

三. 中间操作 intermediate operations

中间操作会返回一个新的流,并且操作是延迟执行的(lazy),它不会修改原始的数据源,而且是由在终点操作开始的时候才真正开始执行。
这个Scala集合的转换操作不同,Scala集合转换操作会生成一个新的中间集合,显而易见Java的这种设计会减少中间对象的生成。
下面介绍流的这些中间操作:

distinct 唯一

distinct保证输出的流中包含唯一的元素,它是通过Object.equals(Object)来检查是否包含相同的元素。

  1. List<String> l = Stream.of("a","b","c","b")
  2. .distinct()
  3. .collect(Collectors.toList());
  4. System.out.println(l); //[a, b, c]

filter 过滤

filter返回的流中只包含满足断言(predicate)的数据。
下面的代码返回流中的偶数集合。

  1. List<Integer> l = IntStream.range(1,10)
  2. .filter( i -> i % 2 == 0)
  3. .boxed()
  4. .collect(Collectors.toList());
  5. System.out.println(l); //[2, 4, 6, 8]

map 映射

map方法将流中的元素映射成另外的值,新的值类型可以和原来的元素的类型不同。
下面的代码中将字符元素映射成它的哈希码(ASCII值)。

  1. List<Integer> l = Stream.of('a','b','c')
  2. .map( c -> c.hashCode())
  3. .collect(Collectors.toList());
  4. System.out.println(l); //[97, 98, 99]

flatmap 映射汇总

flatmap方法混合了map + flattern的功能,同时扩展flatMapToDouble、flatMapToInt、flatMapToLong提供了转换成特定流的方法。它将映射后的流的元素全部放入到一个新的流中。它的方法定义如下:

  1. <R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

flatmap适用于多对多或者一对多的映射关系,mapper函数会将每一个元素转换成一个流对象,而flatMap方法返回的一个流包含所有mapper转换后的元素。
下面举个例子来详细说明:
给定一个列表{“aaa”,”bbb”,”ddd”,”eee”,”ccc”}。需要在控制台直接输出aaabbbdddeeeccc字样
采用map来做

  1. List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc");
  2. //这里采用了两次forEach循环进行输出,显然不太优雅
  3. list.stream().map(x -> {
  4. List<Character> characterList = new ArrayList<>();
  5. char[] chars = x.toCharArray();
  6. for (char c : chars) {
  7. characterList.add(c);
  8. }
  9. return characterList.stream();
  10. }).forEach(xStream -> xStream.forEach(System.out::print)); //aaabbbdddeeeccc

采用flatMap来做

  1. List<String> list = Arrays.asList("aaa", "bbb", "ddd", "eee", "ccc");
  2. //采用flatMap来做 体会一下flatMap的魅力吧
  3. list.stream().flatMap(x -> {
  4. List<Character> characterList = new ArrayList<>();
  5. char[] chars = x.toCharArray();
  6. for (char c : chars) {
  7. characterList.add(c);
  8. }
  9. return characterList.stream();
  10. }).forEach(System.out::print); //aaabbbdddeeeccc

limit 截断

limit方法指定数量的元素的流。对于串行流,这个方法是有效的,这是因为它只需返回前n个元素即可,但是对于有序的并行流,它可能花费相对较长的时间,如果你不在意有序,可以将有序并行流转换为无序的,可以提高性能。

  1. List<Integer> l = IntStream.range(1,100).limit(5)
  2. .boxed()
  3. .collect(Collectors.toList());
  4. System.out.println(l);//[1, 2, 3, 4, 5]

peek 观察者

生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;这里所说的消费函数有点类似于钩子,每个元素被消费时都会执行这个钩子

  1. String[] arr = new String[]{"a","b","c","d"};
  2. Arrays.stream(arr)
  3. .peek(System.out::println) //a,b,c,d
  4. .count();

sorted 排序

sorted()将流中的元素按照自然排序方式进行排序,如果元素没有实现Comparable,则终点操作执行时会抛出java.lang.ClassCastException异常。
sorted(Comparator<? super T> comparator)可以指定排序的方式。
对于有序流,排序是稳定的。对于非有序流,不保证排序稳定。

  1. String[] arr = new String[]{"b_123","c+342","b#632","d_123"};
  2. List<String> l = Arrays.stream(arr)
  3. .sorted((s1,s2) -> {
  4. if (s1.charAt(0) == s2.charAt(0)) {
  5. return s1.substring(2).compareTo(s2.substring(2));
  6. } else {
  7. return s1.charAt(0) - s2.charAt(0);
  8. }
  9. })
  10. .collect(Collectors.toList());
  11. System.out.println(l); //[b_123, b#632, c+342, d_123]

skip 跳过

skip返回丢弃了前n个元素的流,如果流中的元素小于或者等于n,则返回空的流。

  1. String[] arr = new String[]{"a","b","c","d"};
  2. Arrays.stream(arr)
  3. .skip(2)
  4. .forEach(System.out::println);// c d

四. 终点操作 terminal operations

match 断言

  1. public boolean allMatch(Predicate<? super T> predicate)
  2. public boolean anyMatch(Predicate<? super T> predicate)
  3. public boolean noneMatch(Predicate<? super T> predicate)

这一组方法用来检查流中的元素是否满足断言。
allMatch只有在所有的元素都满足断言时才返回true,否则flase,流为空时总是返回true
anyMatch只有在任意一个元素满足断言时就返回true,否则flase,
noneMatch只有在所有的元素都不满足断言时才返回true,否则flase,

  1. System.out.println(Stream.of(1,2,3,4,5).allMatch( i -> i > 0)); //true
  2. System.out.println(Stream.of(1,2,3,4,5).anyMatch( i -> i > 0)); //true
  3. System.out.println(Stream.of(1,2,3,4,5).noneMatch( i -> i > 0)); //false
  4. System.out.println(Stream.<Integer>empty().allMatch( i -> i > 0)); //true
  5. System.out.println(Stream.<Integer>empty().anyMatch( i -> i > 0)); //false
  6. System.out.println(Stream.<Integer>empty().noneMatch( i -> i > 0)); //true

count 计数

count方法返回流中的元素的数量。

  1. String[] arr = new String[]{"a","b","c","d"};
  2. long count = Arrays.stream(arr).count();

你也可以手动来实现它

  1. String[] arr = new String[]{"a","b","c","d"};
  2. long count = Arrays.stream(arr).mapToLong(x->1L).sum();

collect 收集

collect(Collector c) 将流转换为其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法。辅助类Collectors提供了很多的collector收集器,可以满足我们日常的需求,你也可以创建新的collector实现特定的需求。它是一个值得关注的类,你需要熟悉这些特定的收集器,如聚合类averagingInt、最大最小值maxBy minBy、计数counting、分组groupingBy、字符串连接joining、分区partitioningBy、汇总summarizingInt、化简reducing、转换toXXX等。

Collectors里常用搜集器介绍:

方法 返回类型 作用
toList() List 把流中元素收集到List
List result = list.stream().collect(Collectors.toList());
toSet**()** Set 把流中元素收集到Set
Set result = list.stream().collect(Collectors.toSet());
toCollection**()** Collection 把流中元素收集到集合
Collection result = lsit.stream().collect(Collectors.toCollection(ArrayListL::new));
counting**()** Long 计算流中元素的个数
long count = lsit.stream().collect(Collectors.counting());
summingInt**()** Integer 对流中元素的整数属性求和
int total = lsit.stream().collect(Collectors.counting());
averagingInt Double 计算元素Integer属性的均值
double avg = lsit.stream().collect(Collectors.averagingInt(Student::getAge));
summarizingInt IntSummaryStatistics 收集元素Integer属性的统计值
IntSummaryStatistics result = list.stream().collect(Collectors.summarizingInt(Student::getAge));
joining
Stream 连接流中的每个字符串
String str = list.stream().map(Student::getName).collect(Collectors.joining());
maxBy
Optional 根据比较器选择最大值
Opetional max = list.stream().collect(Collectors.maxBy(comparingInt(Student::getAge)))
minBy
Optional 根据比较器选择最小值
Optional min= list.stream().collect(Collectors.minBy(comparingInt(Student::getAge)));
reducing
规约产生的类型 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值
int total = list.stream().collect(Collectors.reducing(0, Student::getAge, Integer::sum));
collectingAndThen 转换函数返回的类型 包裹另一个收集器,对其结果转换
int how = list.stream().collect(Collectors.collectingAndThen(Collectors.toList(), List::size));
groupingBy Map> 根据某属性值对流分组,属性为K,结果为V
Map> map = list.stream().collect(Collectors.groupingBy(Student::getStatus));
partitioningBy Map> 根据true或false进行分区
Map> map = list.stream().collect(Collectors.partitioningBy(Student::getPass));


find 返回

  • findAny()**返回任意一个元素**,如果流为空,返回空的Optional,对于并行流来说,它只需要返回任意一个元素即可,所以性能可能要好于findFirst(),但是有可能多次执行的时候返回的结果不一样。
  • findFirst()**返回第一个元素**,如果流为空,返回空的Optional。

    forEach、forEachOrdered 遍历

    forEach遍历流的每一个元素,执行指定的action。它是一个终点操作,和peek方法不同。这个方法不担保按照流的encounter order顺序执行,如果对于有序流按照它的encounter order顺序执行,你可以使用forEachOrdered方法。
    1. Stream.of(1,2,3,4,5).forEach(System.out::println);

PS: 嵌套遍历(不推荐)

如果要对两个集合进行遍历操作,可以将流嵌套,但是这种遍历的性能跟跟foreach嵌套一样,而且不能进行更复杂的操作,不推荐。

  1. ArrayList<String> list = Lists.newArrayList("1", "2");
  2. ArrayList<String> list2 = Lists.newArrayList("一", "二");
  3. list.stream().forEach(str1->{
  4. list2.stream().forEach(str2->{
  5. System.out.println(str1+str2);
  6. });
  7. });

max、min 最大最小值

max返回流中的最大值,
min返回流中的最小值。

  1. ArrayList<Integer> list = Lists.newArrayList(3,5,2,1);
  2. Integer max = list.stream().max(new Comparator<Integer>() {
  3. @Override
  4. public int compare(Integer o1, Integer o2) {
  5. return o1 - o2;
  6. }
  7. }).get();

reduce 归约

reduce是常用的一个方法,事实上很多操作都是基于它实现的。
它有几个重载方法:

方法 描述
reduce(BinaryOperator b) 可以将流中元素反复结合起来,得到一个值,返回 Optional
reduce(T iden, BinaryOperator b) 可以将流中元素反复结合起来,得到一个值,返回 T
reduce(U identity, BiFunction a, BinaryOperator combiner) 可以将流中元素反复结合起来,得到一个值,返回 Optional

PS: BinaryOperator 函数式接口,也即Lambada表达式
reduce是很重要的一种变成思想。这里重点介绍一下。reduce的作用是把stream中的元素给组合起来。至于怎么组合起来:

  • 它需要我们首先提供一个起始种子,然后依照某种运算规则使其与stream的第一个元素发生关系产生一个新的种子,这个新的种子再紧接着与stream的第二个元素发生关系产生又一个新的种子,就这样依次递归执行,最后产生的结果就是reduce的最终产出,这就是reduce的算法最通俗的描述;

所以运用reduce我们可以做sum,min,max,average,所以这些我们称之为针对具体应用场景的reduce,这些常用的reduce,stream api已经为我们封装了对应的方法。

  1. //求和 sum
  2. List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
  3. // 没有起始值时返回为Optional类型
  4. Optional<Integer> sumOptional = integers.stream().reduce(Integer::sum);
  5. System.out.println(sumOptional.get()); //15
  6. // 可以给一个起始种子值
  7. Integer sumReduce = integers.stream().reduce(0, Integer::sum);
  8. System.out.println(sumReduce); //15
  9. //直接用sum方法
  10. Integer sum = integers.stream().mapToInt(i -> i).sum();
  11. System.out.println(sum); //15

前面两个方法比较简单,重点说说三个参数的reduce(U identity, BiFunction a, BinaryOperator combiner)
三个参数时是最难以理解的。 分析下它的三个参数:

  • identity: 一个初始化的值;这个初始化的值其类型是泛型U,与Reduce方法返回的类型一致;注意此时Stream中元素的类型是T,与U可以不一样也可以一样,这样的话操作空间就大了;不管Stream中存储的元素是什么类型,U都可以是任何类型,如U可以是一些基本数据类型的包装类型Integer、Long等;或者是String,又或者是一些集合类型ArrayList等;后面会说到这些用法。
  • accumulator: 其类型是BiFunction,输入是U与T两个类型的数据,而返回的是U类型;也就是说返回的类型与输入的第一个参数类型是一样的,而输入的第二个参数类型与Stream中元素类型是一样的
  • combiner: 其类型是BinaryOperator,支持的是对U类型的对象进行操作

第三个参数combiner主要是使用在并行计算的场景下;如果Stream是非并行时,它实际上是不生效的。
因此针对这个方法的分析需要分并行与非并行两个场景。

就是因为U和T不一样,所以给了我们更多的发挥。比如设U的类型是ArrayList,那么可以将Stream中所有元素添加到ArrayList中再返回了,如下示例:

  1. ArrayList<String> result = Stream.of("aa", "ab", "c", "ad").reduce(new ArrayList<>(),
  2. (u, s) -> {
  3. u.add(s);
  4. return u;
  5. }, (strings, strings2) -> strings);
  6. System.out.println(result); //[aa, ab, c, ad]

注意由于是非并行的,第三个参数实际上没有什么意义,可以指定r1或者r2为其返回值,甚至可以指定null为返回值。下面看看并行的情况:

当Stream是并行时,第三个参数就有意义了,它会将不同线程计算的结果调用combiner做汇总后返回。注意由于采用了并行计算,前两个参数与非并行时也有了差异! 看个例子:

  1. Integer reduce = Stream.of(1, 2, 3).parallel().reduce(
  2. 4,
  3. (integer, integer2) -> integer + integer2,
  4. (integer, integer2) -> integer + integer2);
  5. System.out.println(reduce); //18

输出:18
omg,结果竟然是18。显然串行的话结果是10;这个不太好理解,但是我下面写一个等价的方式,可以帮助很好的理解这个结果:

  1. Optional<Integer> reduce = Stream.of(1, 2, 3).map(n -> n + 4).reduce((s1, s2) -> s1 + s2);
  2. System.out.println(reduce.get()); //18

这种方式有助于理解并行三个参数时的场景,实际上就是第一步使用accumulator进行转换(它的两个输入参数一个是identity, 一个是序列中的每一个元素),由N个元素得到N个结果;第二步是使用combiner对第一步的N个结果做汇总。

好了,三个参数的reduce先介绍到这。下面继续看看reduce能为我们做什么?

  1. //构造字符串流
  2. List<String> strs = Arrays.asList("H", "E", "L", "L", "O");
  3. // reduce
  4. String concatReduce = strs.stream().reduce("", String::concat);
  5. System.out.println(concatReduce); //HELLO
  6. Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5);
  7. Integer minReduce = integerStream.reduce(Integer.MAX_VALUE, Integer::min);
  8. System.out.println(minReduce); //1

toArray()

将流中的元素放入到一个数组中,默认为Object数组
他还有一个重载方法 A[] toArray(IntFunction generator) 可以返回指定类型的数组

  1. Object[] objects = Stream.of(1, 2, 3, 4, 5).toArray();
  2. Integer[] integers = Stream.of(1, 2, 3, 4, 5).toArray(Integer[]::new);

concat 组合

concat(Stream a, Stream b)用来连接类型一样的两个流。

  1. List<Integer> list1 = Arrays.asList(1,2,3);
  2. List<Integer> list2 = Arrays.asList(4,3,2);
  3. Stream.concat(list1.stream(),list2.stream()).forEach(System.out::print);

toXXX 转换

toArray方法将一个流转换成数组,而如果想转换成其它集合类型,西需要调用collect方法,利用Collectors.toXXX方法进行转换:
image.png

五. 更进一步

虽然Stream提供了很多的操作,但是相对于Scala等语言,似乎还少了一些。一些开源项目提供了额外的一些操作,比如protonpack项目提供了下列方法:

  • takeWhile and takeUntil
  • skipWhile and skipUntil
  • zip and zipWithIndex
  • unfold
  • MapStream
  • aggregate
  • Streamable
  • unique collector

java8-utils 也提供了一些有益的辅助方法。