Java 之惰性计算 stream 总结
Java从8开始,不但引入了Lambda表达式,还引入了一个全新的流式API:Stream API。它位于java.util.stream包中。这个Stream不同于java.io的InputStream和OutputStream,它代表的是任意Java对象的序列,它类似Python中的迭代器。
一. 创建Stream
1.1 Stream.of()
创建Stream
最简单的方式是直接用Stream.of()
静态方法,传入可变参数即创建了一个能输出确定元素的Stream
,虽然这种方式基本上没啥实质性用途,但测试的时候很方便。
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("A", "B", "C", "D");
stream.forEach(System.out::println);
}
}
1.2 基于数组或Collection
- 把数组变成
Stream
使用Arrays.strem()
方法。 - 对于
Collection
(List、Set、Queue
等),直接调用stream()
方法就可以获得Stream
。
import java.util.ArrayList;
import java.util.List;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
stream1.forEach(System.out::println);
List<String> stringOne = new ArrayList<>();
stringOne.add("a");
stringOne.add("b");
stringOne.add("c");
Stream<String> stream2 = stringOne.stream();
stream2.forEach(System.out::println);
Set<String> setOne = new HashSet<>();
setOne.add("1");
setOne.add("2");
setOne.add("3");
Stream<String> stream3 = setOne.stream();
stream3.forEach(System.out::println);
}
}
1.3 基于Supplier
创建Stream
还可以通过Stream.generate()
方法,它需要传入一个Supplier
对象,基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种Stream
保存的不是元素,而是算法,它可以用来表示无限序列。例如,我们编写一个能不断生成自然数的Supplier
,它的代码非常简单,每次调用get()
方法,就生成下一个自然数:
import java.util.function.Supplier;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<Integer> natual = Stream.generate(new NatualSupplier());
natual.limit(20).forEach(System.out::println);
}
}
class NatualSupplier implements Supplier<Integer> {
int n = 0;
public Integer get() {
n++;
return n;
}
}
二. 基本类型
因为Java
的范型不支持基本类型,所以我们无法用Stream<int>
这样的类型,会发生编译错误。为了保存int
,只能使用String<Integer>
,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java
标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream,它们的使用方法和范型Stream
没有大的区别,设计这三个Stream
的目的是提高运行效率:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 }); // 将int[]数组变为IntStream
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong); // 将Stream<String>转换为LongStream
三. 常用Stream函数
3.1 Stream.map()
Stream.map()
是Stream
最常用的一个转换方法,它把一个Stream
转换为另一个Stream
。所谓map
操作,就是把一种操作运算,映射到一个序列的每一个元素上。如果我们查看Stream
的源码,会发现map()
方法接收的对象是Function
接口对象,它定义了一个apply()
方法,负责把一个T
类型转换成R
类型
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
s2.forEach(System.out::println);
}
}
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> s = Stream.of(" Apple ", " pear ", " ORANGE", " BaNaNa ");
s.map(String::trim)
.map(String::toLowerCase)
.map(String::length)
.forEach(System.out::println);
}
}
3.2 Stream.filter()
所谓filter()
操作,就是对一个Stream
的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream
。filter()
方法接收的对象是Predicate
接口对象,它定义了一个test()
方法,负责判断元素是否符合条件。
import java.util.stream.IntStream;
public class Main {
public static void main(String[] args) {
IntStream.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
}
}
注意:filter
的意思是筛选出满足条件的项(返回true
的项)。
3.3 Stream.reduce()
Stream.reduce()
则是Stream
的一个聚合方法,它可以把一个Stream
的所有元素按照聚合函数聚合成一个结果。reduce()
方法传入的对象是BinaryOperator
接口,它定义了一个apply()
方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果。reduce()
操作需要初始化结果为指定值(这里是0)。
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
int sum = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(0, (x, y) -> x + y); // 求 1 到 9 的和
System.out.println(sum);
}
}
注意:reduce()
是聚合方法,聚合方法会立刻对Stream
进行计算。
四. 聚合操作
reduce()
只是一种聚合操作,如果我们希望把Stream
的元素保存到集合,例如List
,因为List
的元素是确定的Java
对象,因此,把Stream
变为List
不是一个转换操作,而是一个聚合操作,它会强制Stream
输出每个元素。
4.1 输出为List
把Stream
的每个元素收集到List
的方法是调用collect()
并传入Collectors.toList()
对象,它实际上是一个Collector
实例,通过类似reduce()
的操作,把每个元素添加到一个收集器中(实际上是ArrayList
)。
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("Apple", null, "Pear", null, "Orange");
List<String> list = stream.filter(s -> s != null).collect(Collectors.toList());
System.out.println(list);
}
}
4.2 输出为Set
Stream<String> stream = Stream.of("Apple", null, "Pear", null, "Orange");
Set<String> set = stream.filter(s -> s != null).collect(Collectors.toSet());
4.3 输出为数组
把Stream
的元素输出为数组和输出为List
类似,我们只需要调用toArray()
方法,并传入数组的“构造方法”。注意到传入的“构造方法”是String[]::new
,它的签名实际上是IntFunction<String[]>
定义的String[] apply(int)
,即传入int
参数,获得String[]
数组的返回值。
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
4.4 输出为Map
如果我们要把Stream
的元素收集到Map
中,就稍微麻烦一点。因为对于每个元素,添加到Map
时需要key
和value
,因此,我们要指定两个映射函数,分别把元素映射为key
和value
:
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("APPL:Apple", "MSFT:Microsoft");
Map<String, String> map = stream
.collect(Collectors.toMap(
s -> s.substring(0, s.indexOf(':')),
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
4.5 输出为分组
Stream
还有一个强大的分组功能,可以按组输出。分组输出使用Collectors.groupingBy()
,它需要提供两个函数:一个是分组的key
,这里使用s -> s.substring(0, 1)
,表示只要首字母相同的String
分到一组,第二个是分组的value
,这里直接使用Collectors.toList()
,表示输出为List
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> list = Arrays.asList("Apple", "Banana", "Blackberry", "Coconut", "Avocado", "Cherry", "Apricots");
Map<String, List<String>> groups = list.stream()
.collect(Collectors.groupingBy(s -> s.substring(0, 1), Collectors.toList()));
System.out.println(groups);
}
}
五. 其他聚合方法
除了reduce()
和collect()
外,Stream
还有一些常用的聚合方法:
count()
:用于返回元素个数;max(Comparator<? super T> cp)
:找出最大元素;min(Comparator<? super T> cp)
:找出最小元素。
针对IntStream、LongStream和DoubleStream
,还额外提供了以下聚合方法:
sum()
:对所有元素求和;average()
:对所有元素求平均数。
还有一些方法,用来测试Stream的元素是否满足以下条件:
boolean allMatch(Predicate<? super T>)
:测试是否所有元素均满足测试条件;boolean anyMatch(Predicate<? super T>)
:测试是否至少有一个元素满足测试条件。
最后一个常用的方法是forEach()
,它可以循环处理Stream
的每个元素,我们经常传入System.out::println
来打印Stream
的元素:
Stream<String> s = ...
s.forEach(str -> {
System.out.println("Hello, " + str);
});
六. 其他操作
6.1 排序
此方法要求Stream
的每个元素必须实现Comparable
接口。如果要自定义排序,传入指定的Comparator
即可。注意sorted()
只是一个转换操作,它会返回一个新的Stream
。
List<String> list = Stream.of("Orange", "apple", "Banana")
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
System.out.println(list);
6.2 去重
Stream.of("A", "B", "A", "C", "B", "D")
.distinct()
.forEach(System.out::println);
6.3 截取
截取操作常用于把一个无限的Stream
转换成有限的Stream
,skip()
用于跳过当前Stream
的前N
个元素,limit()
用于截取当前Stream
最多前N
个元素,截取操作也是一个转换操作,将返回新的Stream
。
Stream.of("A", "B", "C", "D", "E", "F")
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.collect(Collectors.toList()); // [C, D, E]
6.4 合并
将两个Stream
合并为一个Stream
可以使用Stream
的静态方法concat()
Stream<String> s1 = Stream.of("A", "B", "C");
Stream<String> s2 = Stream.of("D", "E");
Stream<String> s = Stream.concat(s1, s2);
s.forEach(System.out::println); // [A, B, C, D, E]
6.5 flatMap
如果Stream
的元素是集合,而我们希望把上述Stream
转换为Stream<Integer>
,就可以使用flatMap()
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
Stream<Integer> i = s.flatMap(list -> list.stream());
因此,所谓flatMap()
,是指把Stream
的每个元素(这里是List
)映射为Stream
,然后合并成一个新的Stream
┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
│
│flatMap(List -> Stream)
│
▼
┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
6.6 并行
通常情况下,对Stream
的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream
的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。把一个普通Stream
转换为可以并行处理的Stream
非常简单,只需要用parallel()
进行转换。经过parallel()
转换后的Stream
只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);