函数式数据处理

2020-07-01

1. Stream

概念:

  • Stream:支持数据处理操作的源(集合、数组、输入/输出资源)生成的元素序列 。
  • 备注: 流是Java 8 API的新成员,它允许你以声明性方式处理数据集合。
  • Lambda表达式博客链接

定义:

  1. 源: 流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。
  2. 元素序列: 就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如ArrayList 与 LinkedList)。但流的目的在于表达计算,比如你前面见到的filter、sorted和map。集合讲的是数据,流讲的是计算。
  3. 数据处理操作: 流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可并行执行。

特点:

  1. 流水线: 很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。
  2. 内部迭代: 与使用迭代器显式迭代的集合不同,流的迭代操作是在背后进行的。

流的特性:

  1. 只能遍历一次。和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。你可以从原始数据源那里再获得一个新的流来重新遍历一遍,就像迭代器一样(这里假设它是集合之类的可重复的源,如果是I/O通道就没戏了)。

Stream API 带来的好处:

  • 声明性 : 更简洁易读
  • 可复合 : 更灵活
  • 可并行 : 性能更好

流与集合

粗略地说,集合与流之间的差异就在于什么时候进行计算。集合是一个内存中的数据结构,集合中的每个元素都得先计算出来才能添加到集合中。相比之下,流则是在概念上固定的数据结构(你不能添加或删除元素),其元素则是按需计算的。

流与集合的差异:

  1. 遍历数据的方式不同。使用Collection接口需要用户去做迭代(比如用for-each),这称为外部迭代。相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。
  2. //TODO 待补充

2. 使用 Stream

API

操作 类型 返回类型 函数式接口 函数描述符
filter 中间 Stream Predicate T -> boolean
distinct 中间(有状态 & 无界) Stream
skip 中间(有状态 & 有界) Stream long
limit 中间(有状态 & 有界) Stream long
map 中间 Stream Function<T, R> T -> R
flatMap 中间 Stream Function<T, Stream> T -> Stream
sorted 中间(有状态 & 无界) Stream Comparator (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<T, A, R>
reduce 终端(有状态 & 有界) Optional BinaryOperator (T, T) -> T
count 终端 long

2.1 filter

过滤流元素:该操作会接受一个 Predicate 函数式接口对象(一个返回boolean的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//过滤出元素小于5的集合
List<Integer> result = numbers.stream().filter(number -> number < 5).collect(Collectors.toList());
System.out.println(result.toString());
}

//输出结果
[1, 2, 3, 4]

2.2 distinct

对流元素去重:根据流元素的hashCode和equals方法判断元素是否重复。

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 1, 1, 2, 2, 2, 3, 3);
//去重数据
List<Integer> result = numbers.stream().distinct().collect(Collectors.toList());
System.out.println(result.toString());
}

//输出结果
[1, 2, 3]

2.3 limit

截断流:该操作会接受一个 int 类型的入参,返回一个不超过给定长度的流。(如果流是有序的,则最多返回前 n 个元素)。

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//截断前2个元素
List<Integer> result = numbers.stream().limit(2).collect(Collectors.toList());
System.out.println(result.toString());
}

//输出结果
[1, 2]

2.4 skip

跳过元素:该操作会接受一个 int 类型的入参,返回扔掉前 n 个元素的流。

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//跳过前2个元素,返回剩余元素的集合
List<Integer> result = numbers.stream().skip(2).collect(Collectors.toList());
System.out.println(result.toString());
}

//输出结果
[3, 4, 5, 6, 7, 8]

2.5 map

映射元素: 它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素。

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//将int元素映射成String元素
List<String> result = numbers.stream().map(Object::toString).collect(Collectors.toList());
System.out.println(result.toString());
}

//输出结果
[1, 2, 3, 4, 5, 6, 7, 8]

2.6 flatMap

扁平化流: 当流的元素可以转换成另外一个流时,扁平化流会得到一个流元素的流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//举例
public static void main(String[] args) {
List<String> a = Arrays.asList("a", "a1");
List<String> b = Arrays.asList("b", "b1");
List<String> c = Arrays.asList("c", "c1");

List<List<String>> listList = Arrays.asList(a, b, c);
//扁平化流:将 Stream<List<String>> 流转换成一个新的 Stream<String> 流。
List<String> list = listList.stream().flatMap(List::stream).collect(Collectors.toList());
System.out.println(list.toString());
}

//输出结果:
[a, a1, b, b1, c, c1]

2.7 anyMatch

判断流中是否存在元素与谓词(Predicate)匹配:接受一个 Predicate 对象,返回流元素是否存在与谓词匹配。(注意:该方法执行时,若找到一个相匹配的元素,则终止操作,并返回true)

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
if (numbers.stream().anyMatch(number -> number == 2)) {
System.out.println("成功匹配到数字2");
}
}

//输出结果:
成功匹配到数字2

2.8 allMatch

判断流中所有元素是否都与谓词(Predicate)匹配:接受一个 Predicate 对象,返回流元素是否都与谓词匹配。 (注意:该方法执行时,若找到一个不匹配的元素,则终止操作,并返回false)

1
2
3
4
5
6
7
8
9
10
11
//举例
public static void main(String[] args) {
List<String> list = Arrays.asList("1,", "2,", "3,", "4,", "5,");
//判断元素是否都以','结尾
if (list.stream().allMatch(s -> s.endsWith(","))) {
System.out.println("所有元素均以','结尾");
}
}

//输出结果:
所有元素均以','结尾

2.9 nonMatch

与allMatch相反,nonMatch是判断流中所有元素是否都不与谓词(Predicate)匹配:接受一个 Predicate 对象,返回流元素是否都不与谓词匹配。 (注意:该方法执行时,若找到一个相匹配的元素,则终止操作,并返回false)

1
2
3
4
5
6
7
8
9
10
11
//举例
public static void main(String[] args) {
List<String> list = Arrays.asList("1,", "2,", "3,", "4,", "5,");
//判断元素是否都以' '结尾
if (list.stream().noneMatch(s -> s.endsWith(" "))) {
System.out.println("所有元素都不以' '结尾");
}
}

//输出结果:
所有元素都不以' '结尾

2.10 findFirst

找到流中第一个元素,返回一个 Optional 对象。一般情况下配合filter使用,筛选流中元素后得到流中第一个元素。(注意:该方法执行时,找到第一个元素之后,则终止操作,并返回包含第一个元素的Optional对象)

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//找到第一个小于5的元素
Optional<Integer> number = numbers.stream().filter(n -> n < 5).findFirst();
System.out.println(number.orElse(null));
}

//输出结果:
1

2.11 findAny

找到流中任何一个元素,返回一个 Optional 对象。一般情况下配合filter使用,筛选流中元素后得到流中任何一个元素。(注意:该方法执行时,找到任何一个元素之后,则终止操作,并返回包含匹配的元素的Optional对象。该方法在并行情况下效率更佳)

1
2
3
4
5
6
7
8
9
10
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//找到任何一个小于5的元素
Optional<Integer> number = numbers.stream().filter(n -> n < 5).findAny();
System.out.println(number.orElse(null));
}

//输出结果:
1

何时使用 findFirst 和 findAny

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

2.12 reduce

规约:包含2个方法

第一个方法

该方法接受一个与流元素类型相同的泛型对象 T 和一个 BinaryOperator(该类继承了BiFunction)对象;返回一个与流元素类型相同的规约后的对象。

1
2
//Stream 源码
T reduce(T identity, BinaryOperator<T> accumulator);

举例:对集合内元素求和

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

//输出结果
36

举例:对集合内元素求积
1
2
3
4
5
6
7
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
int number = numbers.stream().reduce(1, (a, b) -> a * b);
System.out.println(number);
}
//输出结果
40320

第二个方法

该方法接受一个 BinaryOperator 对象;返回 Optional 对象。

1
2
//Stream 源码
Optional<T> reduce(BinaryOperator<T> accumulator);

举例:对集合内元素求和

1
2
3
4
5
6
7
8
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> numberOptional = numbers.stream().reduce(Integer::sum);
System.out.println(numberOptional.orElse(0));
}

//输出结果
36

举例:对集合内元素求积
1
2
3
4
5
6
7
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> numberOptional = numbers.stream().reduce((a, b) -> a * b);
System.out.println(numberOptional.orElse(0));
}
//输出结果
40320

举例:对集合内元素最大值(若需要并行执行,可使用并行流parallelStream())
1
2
3
4
5
6
7
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> numberOptional = numbers.stream().reduce(Integer:max);
System.out.println(numberOptional.orElse(0));
}
//输出结果
8

举例:对集合内元素最小值(若需要并行执行,可使用并行流parallelStream())
1
2
3
4
5
6
7
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> numberOptional = numbers.stream().reduce(Integer:min);
System.out.println(numberOptional.orElse(0));
}
//输出结果
1

2个方法的区别

第一个方法接受了一个对象,执行规约方法时会将入参与流中的元素规约到一起。
第二个方法只对流中的元素进行规约,因为源可能是空的,所以返回一个Optional对象。

2.13 count

对流中元素进行计数,返回一个long类型的对象。

举例:

1
2
3
4
5
6
7
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
long count = numbers.stream().count();
System.out.println(count));
}
//输出结果
8

2.14 注意:

  • filter、sorted、map和collect等操作是与具体线程模型无关的高层次构件,所以它们的内部实现可以是单线程的,也可能透明地充分利用你的多核架构!在实践中,这意味着你用不着为了让某些数据处理任务并行而去操心线程和锁了,Stream API都替你做好了!
  • 对于流而言,某些操作(例如allMatch、anyMatch、noneMatch、findFirst和findAny)不用处理整个流就能得到结果。只要找到一个元素,就可以有结果了。同样,limit也是一个短路操作:它只需要创建一个给定大小的流,而用不着处理流中所有的元素。在碰到无限大小的流的时候,这种操作就有用了:它们可以把无限流变成有限流

3. 数值流

我们在计算流中元素总和时,通常情况下会这样做:

1
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8).reduce(0, Integer::sum);

这段代码问题是它暗藏拆箱成本。每个Integer都必须拆成一个原始类型再进行求和。

3.1 原始类型特化

说明:

Java 8 引入了三个原始类型特化流接口来解决上面这个暗藏拆箱成本的问题:IntStream、DoubleStream和LongStream,分别将流中的元素特化为int、long和double,从而避免了暗含的拆箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的sum,找到最大元素的max。此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似int和Integer之间的效率差异。

映射到数值流

  • mapToInt: 映射成IntStream
  • mapToLong: 映射成LongStream
  • mapToDouble: 映射成DoubleStream

举例:

1
2
3
4
5
6
public static void main(String[] args) {
//映射到数值流
System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8).mapToInt(n -> n).sum());
System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8).mapToLong(n -> n).sum());
System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8).mapToDouble(n -> n).sum());
}

转换回对象流

通过boxed()方法,转换回对象流,例如:

1
2
3
4
5
6
public static void main(String[] args) {
//通过boxed()方法,转换回对象流
System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8).mapToInt(n -> n).boxed().reduce(0, Integer::sum));
System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8).mapToLong(n -> n).boxed().reduce(0L, Long::sum));
System.out.println(Stream.of(1, 2, 3, 4, 5, 6, 7, 8).mapToDouble(n -> n).boxed().reduce(0D, Double::sum));
}

默认值

对于三种原始流特化,也分别有一个Optional原始类型特化版本:OptionalInt、OptionalDouble和OptionalLong。

数值范围

通过使用range() 和 rangeClosed() 方法生成数值范围。

  • range(int startInclusive, int endExclusive) 方法: 生成 [startInclusive, endExclusive) 范围内数值流(左闭右开)。
  • rangeClosed(int startInclusive, int endInclusive) 方法: 生成 [startInclusive, endInclusive] 范围内数值流(左闭右闭)。

举例:

1
2
3
4
5
6
7
8
public static void main(String[] args) {
System.out.println(IntStream.range(1, 100).count());
System.out.println(IntStream.rangeClosed(1, 100).count());
}

//输出结果
99
100

4. 构建流

这里主要介绍由集合、数值、数组、文件来创建流;最后介绍下由生成函数来创建无限流。

4.1 由集合生成流

Java 8 的 Collection 新增了 stream() 的 Api,集合对象通过调用 stream() 方法生成流。

举例:

1
2
List<String> list = Arrays.asList("sungm", "other");
list.stream();

4.2 由数值生成流

通过 Stream.of() 方法生成流

举例:

1
Stream<String> stream = Stream.of("sungm", "other");

4.3 由数组生成流

通过 Arrays.stream() 方法生成流

举例:

1
2
int[] numbers = {1, 2, 3};
Arrays.stream(numbers);

4.4 由文件生成流

java nio

Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files中的很多静态方法都会返回一个流。例如:Files.lines()方法

4.5 函数生成流:创建无限流

Stream.iterate()方法和Stream.generate()方法:

Stream API提供了两个静态方法来从函数生成流:Stream.iterate和Stream.generate。这两个操作可以创建所谓的无限流。

举例:

1
2
//我们通过使用limit()方法来截断流
Stream.iterate(0, n -> n + 2).limit(10).forEach(System.out::println);
1
Stream.generate(Math::random).limit(5).forEach(System.out::println);

备注:

无限流是没有固定大小的流


5. 用流收集数据

5.1 Collectors (收集器)

简介

  • Stream类的collect方法是一个终端操作,类似于Stream类的reduce方法,可以接受做法作为参数,将流中的元素累积成一个汇总结果,而collect方法接受的就是一个 Collector 对象。
  • 在需要将流项目重组成集合时,一般会使用收集器(Stream方法collect的参数);再宽泛一点来说,但凡要把流中所有的项目合并成一个结果时就可以用。
  • Collectors 非常有用,因为它可以简洁和灵活的定义collect用来生成结果集合的标准。更具体地说,对流调用collect方法将对流中的元素触发一个归约操作(由Collector来参数化)
  • Collectors 是 Collector 的工厂类。

预定义收集器

Collectors类提供的工厂方法(例如groupingBy)创建的收集器。它们主要提供了三大功能:

将流元素归约和汇总为一个值
元素分组
元素分区

5.2 归约和汇总

5.2.1 counting (计数)
1
2
3
4
5
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
long count = numbers.stream().collect(Collectors.counting());
}

说明:

使用 number.stream().count() 进行计数更直接,但是counting收集器在和其他收集器联合使用的时候特别有用。

5.2.2 maxBy (最大值)
1
2
3
4
5
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> maxNumber = numbers.stream().collect(Collectors.maxBy(Comparator.comparingInt(a -> a)));
}

说明:

也可以使用 numbers.stream().max(Comparator.comparingInt(a -> a)) 获取最大值

5.2.3 minBy (最小值)
1
2
3
4
5
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
Optional<Integer> minNumber = numbers.stream().collect(Collectors.minBy(Comparator.comparingInt(a -> a)));
}

说明:

也可以使用 numbers.stream().min(Comparator.comparingInt(a -> a)) 获取最小值

5.2.4 summingInt、summarizingLong、summarizingDouble (求和)
1
2
3
4
5
6
7
8
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//求和
numbers.stream().collect(Collectors.summarizingInt(a -> a));
numbers.stream().collect(Collectors.summarizingLong(a -> (long) a));
numbers.stream().collect(Collectors.summarizingDouble(a -> (double) a));
}

说明:

也可以将流转换成数值流之后再对数值流进行求和,例如: numbers.stream().maoToInt(a -> a).sum()。

5.2.5 averagingInt、averagingLong、averagingDouble (求平均值)
1
2
3
4
5
6
7
8
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//求平均值
double averagingInt = numbers.stream().collect(Collectors.averagingInt(a -> a));
double averagingLong = numbers.stream().collect(Collectors.averagingLong(a -> (long) a));
double averagingDouble = numbers.stream().collect(Collectors.averagingDouble(a -> (double) a));
}
5.2.6 summarizingInt、summarizingLong、summarizingDouble (对数值进行总结)
1
2
3
4
5
6
7
8
9
10
11
12
//举例
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//对数值进行总结,获取流元素的数值和、最大值、最小值、平均值、计数。
IntSummaryStatistics intSummaryStatistics = numbers.stream().collect(Collectors.summarizingInt(a -> a));
LongSummaryStatistics longSummaryStatistics = numbers.stream().collect(Collectors.summarizingLong(a -> (long) a));
DoubleSummaryStatistics doubleSummaryStatistics = numbers.stream().collect(Collectors.summarizingDouble(a -> (double) a));

System.out.println(intSummaryStatistics.toString());
System.out.println(longSummaryStatistics.toString());
System.out.println(doubleSummaryStatistics.toString());
}
1
2
3
4
//输出结果
IntSummaryStatistics{count=8, sum=36, min=1, average=4.500000, max=8}
LongSummaryStatistics{count=8, sum=36, min=1, average=4.500000, max=8}
DoubleSummaryStatistics{count=8, sum=36.000000, min=1.000000, average=4.500000, max=8.000000}

说明:

通过一次 summarizing 操作你可以就计算出元素的个数,并得到元素总和、平均值、最大值和最小值。

5.2.7 join 连接字符串
1
2
3
4
5
6
7
8
//举例
public static void main(String[] args) {
List<String> list = Arrays.asList("One", "Two", "Three", "Four", "Five", "Six");
//直接连接字符串
System.out.println(list.stream().collect(Collectors.joining()));
//字符串之间用指定的delimiter(定界符)连接字符
System.out.println(list.stream().collect(Collectors.joining(", ")));
}
1
2
3
//输出结果
OneTwoThreeFourFiveSix
One, Two, Three, Four, Five, Six

说明:

某些情况下,可以使用String.join()方法更直接。 例如:String.join(“, “, list);

5.2.8 reducing 规约(5.2章节的重点)

Collectors类中存在3个reducing()方法,下面我们逐个介绍

  • reducing(Object, Function, BinaryOperator)
  • reducing(Object, BinaryOperator)
  • reducing(BinaryOperator)

说明:

我们上面讨论的7种方法(5.2.1 - 5.2.7)都是一个可以用 reducing 工厂方法定义的归约过程的特殊情况而已。Collectors.reducing 工厂方法是所有这些特殊情况的一般化。
Collectors类中存在3个reducing()方法功能类似。

方法一:reducing(Object, Function, BinaryOperator) 方法

1
2
3
4
5
6
7
8
9
10
11
//源码
public static <T, U>
Collector<T, ?, U> reducing(U identity,
Function<? super T, ? extends U> mapper,
BinaryOperator<U> op) {
return new CollectorImpl<>(
boxSupplier(identity),
(a, t) -> { a[0] = op.apply(a[0], mapper.apply(t)); },
(a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
a -> a[0], CH_NOID);
}

说明:

  • 该方法第一个参数是泛型对象 U, U 表示规约操作的的初始值,也就是当流中没有元素时的返回值。
  • 该方法第二个参数是函数式接口Function对象,该Function对象将流元素对象转换成你所需要进行规约操作的对象
  • 该方法第三个参数是函数式接口BinaryOperator(二元运算符,BinaryOperator 继承了BiFunction)对象,该BinaryOperator将2个需要进行规约操作的对象,按照BinaryOperator规约成一个对象

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 成绩
*
* @author : sungm
* @date : 2020-07-05 14:32
*/
public class Grade {

private String studentName;
private String course;
private Double score;

//省略getter、setter、toString、以及无参构造方法和全参构造方法

}

/**
* 程序入口
*/
public static void main(String[] args) {
List<Grade> grades = Arrays.asList(new Grade("sungm", "语文", 80.0)
, new Grade("sungm", "数学", 88.5)
, new Grade("sungm", "英语", 88.0)
, new Grade("zhangsan", "语文", 58.0)
, new Grade("zhangsan", "数学", 51.0)
, new Grade("zhangsan", "英语", 61.0)
, new Grade("diLiReBa", "语文", 98.0)
, new Grade("diLiReBa", "数学", 95.0)
, new Grade("diLiReBa", "英语", 100.0));

//所有学生所有课程的总成绩
double score = grades.stream().collect(Collectors.reducing(0D, Grade::getScore, Double::sum));
}

方法二:reducing(Object, BinaryOperator) 方法

1
2
3
4
5
6
7
8
9
10
//源码
public static <T> Collector<T, ?, T>
reducing(T identity, BinaryOperator<T> op) {
return new CollectorImpl<>(
boxSupplier(identity),
(a, t) -> { a[0] = op.apply(a[0], t); },
(a, b) -> { a[0] = op.apply(a[0], b[0]); return a; },
a -> a[0],
CH_NOID);
}

说明:

  • 该方法第一个参数是泛型对象 U, U 表示规约操作的的初始值,也就是当流中没有元素时的返回值。
  • 该方法第二个参数是函数式接口BinaryOperator(二元运算符,BinaryOperator 继承了BiFunction)对象,该BinaryOperator将2个需要进行规约操作的对象,按照BinaryOperator规约成一个对象

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
List<Grade> grades = Arrays.asList(new Grade("sungm", "语文", 80.0)
, new Grade("sungm", "数学", 88.5)
, new Grade("sungm", "英语", 88.0)
, new Grade("zhangsan", "语文", 58.0)
, new Grade("zhangsan", "数学", 51.0)
, new Grade("zhangsan", "英语", 61.0)
, new Grade("diLiReBa", "语文", 98.0)
, new Grade("diLiReBa", "数学", 95.0)
, new Grade("diLiReBa", "英语", 100.0));

//所有学生所有课程的总成绩
double score = grades.stream().map(Grade::getScore).collect(Collectors.reducing(0D, Double::sum));
}

方法三:reducing(BinaryOperator) 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//源码
public static <T> Collector<T, ?, Optional<T>>
reducing(BinaryOperator<T> op) {
class OptionalBox implements Consumer<T> {
T value = null;
boolean present = false;

@Override
public void accept(T t) {
if (present) {
value = op.apply(value, t);
}
else {
value = t;
present = true;
}
}
}

return new CollectorImpl<T, OptionalBox, Optional<T>>(
OptionalBox::new, OptionalBox::accept,
(a, b) -> { if (b.present) a.accept(b.value); return a; },
a -> Optional.ofNullable(a.value), CH_NOID);
}

说明:

  • 该方法入参是函数式接口BinaryOperator(二元运算符,BinaryOperator 继承了BiFunction)对象,该BinaryOperator将2个需要进行规约操作的对象,按照BinaryOperator规约成一个对象
  • 该方法返回一个Optional类的对象。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
List<Grade> grades = Arrays.asList(new Grade("sungm", "语文", 80.0)
, new Grade("sungm", "数学", 88.5)
, new Grade("sungm", "英语", 88.0)
, new Grade("zhangsan", "语文", 58.0)
, new Grade("zhangsan", "数学", 51.0)
, new Grade("zhangsan", "英语", 61.0)
, new Grade("diLiReBa", "语文", 98.0)
, new Grade("diLiReBa", "数学", 95.0)
, new Grade("diLiReBa", "英语", 100.0));

//所有学视所有课程的总成绩
Optional<Double> score = grades.stream().map(Grade::getScore).collect(Collectors.reducing(Double::sum));
}

5.2.9 Stream类的reduce方法 与 Collectors类的reduce方法 的区别

示例:

1
2
3
4
5
6
7
8
9
IntStream.rangeClosed(1, 8)
.boxed()
.reduce(new ArrayList<>(), (List<Integer> l, Integer e) -> {
l.add(e);
return l;
}, (List<Integer> l1, List<Integer> l2) -> {
l1.addAll(l2);
return l1;
});

区别:

Stream类的reduce方法旨在把两个值结合起来生成一个新值,它是一个不可变的归约。与此相反,collect方法的设计就是要改变容器,从而累积要输出的结果。这意味着,上面的示例的代码片段是在滥用reduce方法,因为它在原地改变了作为累加器的List。
以错误的语义使用Stream类的reduce方法还会造成一个实际问题:这个归约过程不能并行工作,因为由多个线程并发修改同一个数据结构可能会破坏List本身。在这种情况下,如果你想要线程安全,就需要每次分配一个新的List,而对象分配又会影响性能。这就是collect方法特别适合表达可变容器上的归约的原因,更关键的是它适合并行操作

使用收集器的的好处:

  • 灵活性更强:可以以不同的方法执行同样的操作。
  • 根据情况选择最佳解决方案:收集器在某种程度上比Stream接口上直接提供的方法用起来更复杂,但好处在于它们能提供更高水平的抽象和概括,也更容易重用和自定义。

建议:

就实际应用而言,建议使用Collectors类的reduce方法,无轮从可读性还是性能上讲,Collectors类的reduce方法都更好。

5.3 分组

5.3.1 groupingBy 分组

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
List<Student> students = Arrays.asList(new Student(1L, "miracle", 25, "Boy")
, new Student(2L, "bob", 24, "Boy")
, new Student(3L, "lucky", 22, "Girl")
, new Student(3L, "lusa", 24, "Girl"));
//按照性别分组
Map<String, List<Student>> map = students.stream().collect(Collectors.groupingBy(Student::getSex));
System.out.println(map.toString());
}

//输出结果:
{
Girl=[Student{id=3, name='lucky', age=22, sex='Girl'}, Student{id=3, name='lusa', age=24, sex='Girl'}],
Boy=[Student{id=1, name='miracle', age=25, sex='Boy'}, Student{id=2, name='bob', age=24, sex='Boy'}]
}

说明:

示例中给groupingBy传入一个Function,我们把这个Function叫作分类函数,因为它用来把流中的元素分成不同的组。

groupingBy源码:

1
2
3
4
public static <T, K> Collector<T, ?, Map<K, List<T>>>
groupingBy(Function<? super T, ? extends K> classifier) {
return groupingBy(classifier, toList());
}