1.1 Stream的创建
1.1.1 Stream.of()
创建Stream最简单的方式是直接用Stream.of()
静态方法,传入可变参数即创建了一个能输出确定元素的Stream:
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("A", "B", "C", "D");
// forEach()方法相当于内部循环调用,
// 可传入符合Consumer接口的void accept(T t)的方法引用:
stream.forEach(System.out::println);
}
}
1.1.2 基于数组
或Collection
第二种创建 Stream的方法是基于一个数组
或者Collection
,这样该Stream输出的元素就是数组或者Collection持有的元素:
public class Main {
public static void main(String[] args) {
Stream<String> stream1 = Arrays.stream(new String[] { "A", "B", "C" });
Stream<String> stream2 = List.of("X", "Y", "Z").stream();
stream1.forEach(System.out::println);
stream2.forEach(System.out::println);
}
}
把数组变成Stream使用Arrays.stream()
方法。对于Collection(List、Set、Queue等)
,直接调用stream()
方法就可以获得Stream
。
上述创建Stream的方法都是把一个现有的序列变为Stream,它的元素是固定的。
1.1.3 基于Supplier
创建Stream还可以通过Stream.generate()
方法,它需要传入一个Supplier
对象:
public static<T> Stream<T> generate(Supplier<T> s) {
Objects.requireNonNull(s);
return StreamSupport.stream(
new StreamSpliterators.InfiniteSupplyingSpliterator.OfRef<>(Long.MAX_VALUE, s), false);
}
基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种Stream保存的不是元素,而是算法,它可以用来表示无限序列。
例如,我们编写一个能不断生成自然数的Supplier,它的代码非常简单,每次调用get()方法,就生成下一个自然数:
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;
}
}
上述代码我们用一个 Supplier<Integer>
模拟了一个无限序列(当然受int范围限制不是真的无限大)。如果用List
表示,即便在int
范围内,也会占用巨大的内存,而Stream
几乎不占用空间,因为每个元素都是实时计算出来的,用的时候再算。
对于无限序列,如果直接调用forEach()
或者count()
这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()
方法可以截取前面若干个元素,这样就变成了一个有限序列,对这个有限序列调用forEach()
或者count()
操作就没有问题。
1.1.4 基于UnaryOperator
创建Stream还可以通过Stream.iterate()
方法,它需要传入一个UnaryOperator
对象:
public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f) {
Objects.requireNonNull(f);
final Iterator<T> iterator = new Iterator<T>() {
@SuppressWarnings("unchecked")
T t = (T) Streams.NONE;
@Override
public boolean hasNext() {
return true;
}
@Override
public T next() {
return t = (t == Streams.NONE) ? seed : f.apply(t);
}
};
return StreamSupport.stream(Spliterators.spliteratorUnknownSize(
iterator,
Spliterator.ORDERED | Spliterator.IMMUTABLE), false);
}
UnaryOperate
(一元操作符) 是Function
的子接口,既需要通过R apply(T t)
产生元素
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
}
基于UnaryOperate
创建的Stream
会不断调用UnaryOperate.apply(t)
方法来不断产生下一个元素,这种Stream
保存的不是元素,而是算法,它也可以用来表示无限序列。
例如,我们编写一个能不断生成自然数的Stream
,只需要传入与 Function
的apply
方法签名一致的lambda。它的代码非常简单:
//Stream.iterate(initial value, next value)
Stream.iterate(0, n -> n + 1)
.limit(10)
.forEach(x -> System.out.println(x));
又如获取一个斐波拉契数列
Stream.iterate(new int[]{0, 1}, n -> new int[]{n[1], n[0] + n[1]})
.limit(20)
.map(n -> n[0])
.forEach(x -> System.out.println(x));
1.1.5 其他方法
创建Stream
的第四种方法是通过一些API提供的接口,直接获得Stream。
例如,Files
类的lines()
方法可以把一个文件变成一个Stream
,每个元素代表文件的一行内容:
Stream<String> lines = Files.lines(Paths.get(""));
此方法对于按行遍历文本文件十分有用。
另外,正则表达式的Pattern
对象有一个splitAsStream()
方法,可以直接把一个长字符串分割成Stream
序列而不是数组:
Pattern pattern = Pattern.compile("");
Stream<String> stream = pattern.splitAsStream("");
1.1.6 补充说明
因为Java的范型不支持基本类型,所以我们无法用Stream<int>
这样的类型,会发生编译错误。为了保存int,只能使用Stream<Integer>
,但这样会产生频繁的装箱、拆箱操作。为了提高效率,Java标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
,它们的使用方法和范型Stream没有大的区别,设计这三个Stream的目的是提高运行效率:
// 将int[]数组变为IntStream:
IntStream stream = Arrays.stream(new int[]{1, 2, 3, 4, 5});
//将Stream<String>转换为LongStream:
LongStream longStream = Arrays.asList("1", "2", "3").stream().mapToLong(Long::valueOf);
练习:编写一个能输出斐波拉契数列(Fibonacci)的LongStream:
LongStream longStream = Stream.iterate(new long[]{1, 1}, fib -> new long[]{fib[1], fib[0] + fib[1]}).mapToLong(fib -> fib[0]);
longStream.limit(20).forEach(System.out::println);
2. stream.map()
Stream.map()
是Stream最常用的一个转换方法,它把一个Stream转换为另一个Stream。
所谓map
操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如,对x计算它的平方,可以使用函数f(x) = x * x。我们把这个函数映射到一个序列1,2,3,4,5上,就得到了另一个序列1,4,9,16,25:
可见,map操作,把一个Stream的每个元素一一对应到应用了目标函数的结果上。
Stream<Integer> s = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> s2 = s.map(n -> n * n);
如果我们查看Stream
的源码,会发现map()方法接收的对象是Function
接口对象,它定义了一个apply()
方法,负责把一个T类型转换成R类型:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
其中,Function
的定义是:
@FunctionalInterface
public interface Function<T, R> {
//将T类型转换为R
R apply(T t);
}
利用map()
,不但能完成数学计算,对于字符串操作,以及任何Java对象都是非常有用的。例如:
public class Main {
public static void main(String[] args) {
List.of(" Apple ", " pear ", " ORANGE", " BaNaNa ")
.stream()
.map(String::trim) // 去空格
.map(String::toLowerCase) // 变小写
.forEach(System.out::println); // 打印
}
}
通过若干步map转换,可以写出逻辑简单、清晰的代码。
练习:使用
map()
把一组String转换为LocalDate并打印。
List<LocalDate> list = stream
.map(LocalDate::parse)
.collect(Collectors.toList());
list.stream().forEach(System.out::println);
3. stream.filter()
Stream.filter()
是Stream的另一个常用转换方法。
所谓filter()
操作,就是对一个Stream的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的 Stream.
例如,我们对1,2,3,4,5这个Stream调用filter(),传入的测试函数f(x) = x % 2 != 0
用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5:
用IntStream写出上述逻辑,代码如下:
IntStream.of(1,2,3,4,5,6,7,8,9).filter(n->n % 2 != 0).forEach(System.out::println);
从结果可知,经过filter()
后生成的Stream元素可能变少。
filter()
方法接收的对象是Predicate
接口对象,它定义了一个test()
方法,负责判断元素是否符合条件:
@FunctionalInterface
public interface Predicate<T> {
// 判断元素t是否符合条件:
boolean test(T t);
}
filter()
除了常用于数值外,也可应用于任何Java对象。例如,从一组给定的LocalDate中过滤掉工作日,以便得到休息日:
Stream.generate(new LocalDateSupplier())
.limit(31)
.filter(d->d.getDayOfWeek() == DayOfWeek.SATURDAY || d.getDayOfWeek() == DayOfWeek.SUNDAY)
.forEach(System.out::println);
4. stream.reduce()
map()
和filter()
都是Stream的转换方法,而Stream.reduce()
则是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, (acc, n) -> acc + n);
System.out.println(sum); // 45
}
}
reduce()
方法传入的对象是BinaryOperator
接口,它定义了一个apply()
方法,负责把上次累加的结果和本次的元素 进行运算,并返回累加的结果:
@FunctionalInterface
public interface BinaryOperator<T> {
// Bi操作:两个输入,一个输出
T apply(T t, T u);
}
上述代码看上去不好理解,但我们用for循环改写一下,就容易理解了:
Stream<Integer> stream = ...
int sum = 0;
for (n : stream) {
sum = (sum, n) -> sum + n;
}
可见,reduce()
操作首先初始化结果为指定值(这里是0
),紧接着,reduce()
对每个元素依次调用(acc, n) -> acc + n
,其中,acc
是上次计算的结果.因此,实际上这个reduce()
操作是一个求和。
如果去掉初始值,我们会得到一个Optional<Integer>
:
Optional<Integer> opt = stream.reduce((acc, n) -> acc + n);
if (opt.isPresent()) {
System.out.println(opt.get());
}
这是因为Stream的元素有可能是0个,这样就没法调用reduce()的聚合函数了,因此返回Optional对象,需要进一步判断结果是否存在。
利用reduce()
,我们可以把求和改成求积,代码也十分简单:
int s = IntStream.of(1, 2, 3, 4, 5, 6).reduce(1, (l, r) -> l * r);
System.out.println(s);
注意:计算求积时,初始值必须设置为1。
除了可以对数值进行累积计算外,灵活运用reduce()
也可以对Java对象进行操作。
下面的代码演示了如何将配置文件的每一行配置通过map()
和reduce()
操作聚合成一个Map<String, String>
:
// 按行读取配置文件:
List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
Map<String, String> map = props.stream()
// 把k=v转换为Map[k]=v:
.map(kv -> {
String[] ss = kv.split("\\=", 2);
return Map.of(ss[0], ss[1]);
})
// 把所有Map聚合到一个Map:
.reduce(new HashMap<String, String>(), (m, kv) -> {
m.putAll(kv);
return m;
});
// 打印结果:
map.forEach((k, v) -> {
System.out.println(k + " = " + v);
});
小结:
reduce()
方法将一个Stream的每个元素依次作用于BinaryOperator
,并将结果合并。reduce()
是聚合方法,聚合方法会立刻对Stream进行计算。
5. 输出集合
5.1 操作分类
我们介绍了Stream的几个常见操作:map()
、filter()
、reduce()
。这些操作对Stream来说可以分为两类,一类是转换操作
,即把一个Stream转换为另一个Stream,例如 map()和filter(),另一类是聚合操作
,即对Stream的每个元素进行计算,得到一个确定的结果,例如reduce()。
区分这两种操作是非常重要的,因为 对于Stream来说,对其进行转换操作并不会触发任何计算!
我们可以做个实验
public class Main {
public static void main(String[] args) {
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
System.out.println(s3); // java.util.stream.ReferencePipeline$3@49476842
}
}
class NatualSupplier implements Supplier<Long> {
long n = 0;
public Long get() {
n++;
return n;
}
}
因为s1是一个Long类型的序列,它的元素高达922亿亿
个,但执行上述代码,既不会有任何内存增长,也不会有任何计算,因为转换操作只是保存了转换规则,无论我们对一个Stream转换多少次,都不会有任何实际计算发生。
而聚合操作则不一样,聚合操作会立刻促使Stream输出它的每一个元素,并依次纳入计算,以获得最终结果。所以,对一个Stream进行聚合操作,会触发一系列连锁反应。
Stream<Long> s1 = Stream.generate(new NatualSupplier());
Stream<Long> s2 = s1.map(n -> n * n);
Stream<Long> s3 = s2.map(n -> n - 1);
Stream<Long> s4 = s3.limit(10);
s4.reduce(0, (acc, n) -> acc + n);
我们对s4
进行reduce()
聚合计算,会不断请求s4
输出它的每一个元素。
因为s4
的上游是s3
,它又会向s3
请求元素,导致s3
向s2
请求元素,s2
向s1
请求元素。最终,s1
从Supplier
实例中请求到真正的元素,并经过一系列转换,最终被reduce()
聚合出结果。
可见,聚合操作是真正需要从Stream请求数据的,对一个Stream做聚合计算后,结果就不是一个Stream,而是一个其他的Java对象。
5.2 输出为List
reduce()
只是一种聚合操作,如果我们希望把Stream的元素保存到集合,例如List
,因为List的元素是确定的Java对象,因此,把Stream变为List不是一个转换操作,而是一个聚合操作,它会强制Stream输出每个元素。
下面的代码演示了如何将一组String先过滤掉空字符串,然后把非空字符串保存到List中:
public class Main {
public static void main(String[] args) {
Stream<String> stream = Stream.of("Apple", "", null, "Pear", " ", "Orange");
List<String> list = stream.filter(s -> s != null && !s.isBlank()).collect(Collectors.toList());
System.out.println(list);
}
}
把Stream的每个元素收集到List的方法是调用collect()并传入Collectors.toList()
对象,它实际上是一个Collector
实例,通过类似reduce()
的操作,把每个元素添加到一个收集器中(实际上是ArrayList)。
类似的,collect(Collectors.toSet())
可以把Stream的每个元素收集到Set
中。
5.3 输出为数组
把Stream的元素输出为数组和输出为List类似,我们只需要调用toArray()
方法,并传入数组的“构造方法”:
List<String> list = Arrays.asList("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);// new String[16]
注意
传入的“构造方法”是String[]::new
,它的签名实际上是IntFunction<String[]>
定义的String[] apply(int)
,即传入int参数,获得String[]
数组的返回值(类似new String[16]
)。
5.4 输出为Map
如果我们要把Stream
的元素收集到Map中,就稍微麻烦一点。因为对于每个元素,添加到Map
时需要key
和value
,因此,我们要指定两个映射函数,分别把元素映射为key
和value
:
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)));
map.forEach((k,v) -> System.out.println(k +":" +v));
5.5 分组输出
Stream
还有一个强大的分组功能,可以按组输出。我们看下面的例子:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("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);
}
}
分组输出使用Collectors.groupingBy()
,它需要提供两个函数:一个是分组的key
,这里使用s -> s.substring(0, 1)
,表示只要首字母相同的String分到一组,第二个是分组的value
,这里直接使用Collectors.toList()
,表示输出为List,上述代码运行结果如下:
{A=[Apple, Avocado, Apricots], B=[Banana, Blackberry], C=[Coconut, Cherry]}
可见,结果一共有3组,按"A",“B”,"C"分组,每一组都是一个List。
假设有这样一个Student类,包含学生姓名、班级和成绩:
class Student {
int gradeId; // 年级
int classId; // 班级
String name; // 名字
int score; // 分数
}
如果我们有一个Stream<Student>
,利用分组输出,可以非常简单地按年级或班级把Student归类。
6. 其他操作
我们把Stream提供的操作分为两类:转换操作
和聚合操作
。除了前面介绍的常用操作外,Stream还提供了一系列非常有用的方法。
6.1 排序sorted()
对Stream的元素进行排序十分简单,只需调用sorted()
方法:
public class Main {
public static void main(String[] args) {
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted()
.collect(Collectors.toList());
System.out.println(list);
}
}
此方法要求Stream的每个元素必须实现Comparable
接口。如果要自定义排序,传入指定的Comparator
即可:
List<String> list = List.of("Orange", "apple", "Banana")
.stream()
.sorted(String::compareToIgnoreCase)
.collect(Collectors.toList());
注意sorted()
只是一个转换操作,它会返回一个新的Stream。
6.2 去重distinct()
对一个Stream的元素进行去重,没必要先转换为Set,可以直接用distinct()
:
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList()); //
6.3 截取 limit()
截取操作常用于把一个无限的Stream转换成有限的Stream,skip()
用于跳过当前Stream的前N个元素,limit()
用于截取当前Stream最多前N个元素:
List.of("A", "B", "C", "D", "E", "F")
.stream()
.skip(2) // 跳过A, B
.limit(3) // 截取C, D, E
.collect(Collectors.toList()); // [C, D, E]
截取操作也是一个转换操作,将返回新的Stream
6.4 合并concat()
将两个Stream合并为一个Stream可以使用Stream的静态方法concat()
Stream<String> s1 = List.of("A", "B", "C").stream();
Stream<String> s2 = List.of("D", "E").stream();
// 合并:
Stream<String> s = Stream.concat(s1, s2);
System.out.println(s.collect(Collectors.toList())); // [A, B, C, D, E]
6.5 flatMap
(展开为Stream)
如果Stream的元素是集合:
Stream<List<Integer>> s = Stream.of(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9));
而我们希望把上述Stream转换为Stream<Integer>
,就可以使用flatMap()
:
Stream<Integer> i = s.flatMap(list -> list.stream());
因此,所谓flatMap()
,是指把Stream的每个元素(这里是List)映射为Stream,然后合并成一个新的Stream:
6.3 并行parallel()
通常情况下,对Stream的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。
把一个普通Stream转换为可以并行处理的Stream非常简单,只需要用parallel()
进行转换:
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);
经过parallel()
转换后的Stream只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。
6.4 其他聚合方法
除了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的元素
7. 总结
Stream提供的常用操作有:
● 转换操作:map()
,filter()
,sorted()
,distinct()
;
● 合并操作:concat()
,flatMap()
;
● 并行处理:parallel()
;
● 聚合操作:reduce()
,collect()
,count()
,max()
,min()
,sum()
,average()
;
● 其他操作:allMatch()
, anyMatch()
,forEach()