Stream的使用
Stream
有两种操作,一个是中间操作,每次返回一个新的流,可以有多个;另一个是终端操作,每个流只能进行一次终端操作,终端操作结束后流无法再次使用。终端操作会产生一个新的集合或值
stream
不同于java.io
的InputStream
和OutputStream
,它代表的是任意Java对象的序列。两者对比如下:
java.io | java.util.stream | |
---|---|---|
存储 | 顺序读写的 byte 或 char | 顺序输出任意java对象实例 |
用途 | 序列化至文件或网络 | 内存计算、业务逻辑 |
Stream
和List
也不一样,List存储的每个元素都是已经存储在内存中的某个Java
对象,而Stream
输出的元素可能并没有预先存储在内存中,而是实时计算出来的。
java.util.list | java.util.stream | |
---|---|---|
元素 | 已分配并存储在内存 | 可能未分配,实时计算 |
用途 | 操作一组已存在的Java 对象 | 惰性计算 |
Stream
的特点:它可以“存储”有限个或无限个元素。这里的存储打了个引号,是因为元素有可能已经全部存储在内存中,也有可能是根据需要实时计算出来的。
Stream
的另一个特点是,一个Stream
可以轻易地转换为另一个Stream
,而不是修改原Stream本身。最后,真正的计算通常发生在最后结果的获取,也就是惰性计算。
惰性计算的特点是:一个Stream
转换为另一个Stream
时,实际上只存储了转换规则,并没有任何计算发生。
1、创建Stream
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);
}
}
基于数组或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
。
基于Supplier
创建Stream
还可以通过Stream.generate()
方法,它需要传入一个Supplier
对象:
Stream<String> s = Stream.generate(Supplier<String> sp);
基于Supplier
创建的Stream
会不断调用Supplier.get()
方法来不断产生下一个元素,这种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;
}
}
对于无限序列,如果直接调用forEach()
或者count()
这些最终求值操作,会进入死循环,因为永远无法计算完这个序列,所以正确的方法是先把无限序列变成有限序列,例如,用limit()
方法可以截取前面若干个元素,这样就变成了一个有限序列,对这个有限序列调用forEach()
或者count()
操作就没有问题。
其他方法
创建Stream
的第三种方法是通过一些API
提供的接口,直接获得Stream
。
例如,Files
类的lines()
方法可以把一个文件变成一个Stream
,每个元素代表文件的一行内容:
try (Stream<String> lines = Files.lines(Paths.get("/path/to/file.txt"))) {
...
}
Java
的范型不支持基本类型,所以我们无法用Stream<int>
这样的类型,会发生编译错误。
为了提高效率,Java
标准库提供了IntStream
、LongStream
和DoubleStream
这三种使用基本类型的Stream
,它们的使用方法和范型Stream
没有大的区别,设计这三个Stream
的目的是提高运行效率:
// 将int[]数组变为IntStream:
IntStream is = Arrays.stream(new int[] { 1, 2, 3 });
// 将Stream<String>转换为LongStream:
LongStream ls = List.of("1", "2", "3").stream().mapToLong(Long::parseLong);
小结
创建Stream
的方法有 :
- 通过指定元素、指定数组、指定
Collection
创建Stream
; - 通过
Supplier
创建Stream
,可以是无限序列; - 通过其他类的相关方法创建。
- 基本类型的
Stream
有IntStream
、LongStream
和DoubleStream
。
2、使用map
Stream.map()
是Stream
最常用的一个转换方法,它把一个Stream
转换为另一个Stream
。
所谓map
操作,就是把一种操作运算,映射到一个序列的每一个元素上。例如,对x
计算它的平方,可以使用函数f(x) = x * x
。我们把这个函数映射到一个序列1,2,3,4,5
上,就得到了另一个序列1,4,9,16,25
:
f(x) = x * x
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 2 3 4 5 6 7 8 9 ]
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 4 9 16 25 36 49 64 81 ]
可见,map
操作,把一个Stream
的每个元素一一对应到应用了目标函数的结果上。
JDK9
在 List
、Set
、Map
等,都提供了 of()
方法,表面上看來,它们似乎只是建立 List
、Set
、Map
实例的便捷方法
利用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); // 打印
}
}
// 输出
apple
pear
orange
banana
3、使用 filter
Stream.filter()
是Stream
的另一个常用转换方法。
所谓filter()
操作,就是对一个Stream
的所有元素一一进行测试,不满足条件的就被“滤掉”了,剩下的满足条件的元素就构成了一个新的Stream
。
例如,我们对1,2,3,4,5
这个Stream
调用filter()
,传入的测试函数f(x) = x % 2 != 0
用来判断元素是否是奇数,这样就过滤掉偶数,只剩下奇数,因此我们得到了另一个序列1,3,5
:
f(x) = x % 2 != 0
│
│
┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
[ 1 2 3 4 5 6 7 8 9 ]
│ X │ X │ X │ X │
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
[ 1 3 5 7 9 ]
用IntStream
写出上述逻辑,代码如下:
public class Main {
public static void main(String[] args) {
IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9)
.filter(n -> n % 2 != 0)
.forEach(System.out::println);
}
}
4、使用reduce
map()
和filter()
都是Stream
的转换方法,而Stream.reduce()
则是Stream
的一个聚合方法,它可以把一个Stream
的所有元素按照聚合函数聚合成一个结果。
可见,reduce()
操作首先初始化结果为指定值(这里是0),紧接着,reduce()
对每个元素依次调用(acc, n) -> acc + n
,其中,acc
是上次计算的结果:
import java.util.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
}
}
// 计算过程:
acc = 0 // 初始化为指定值
acc = acc + n = 0 + 1 = 1 // n = 1
acc = acc + n = 1 + 2 = 3 // n = 2
acc = acc + n = 3 + 3 = 6 // n = 3
acc = acc + n = 6 + 4 = 10 // n = 4
acc = acc + n = 10 + 5 = 15 // n = 5
acc = acc + n = 15 + 6 = 21 // n = 6
acc = acc + n = 21 + 7 = 28 // n = 7
acc = acc + n = 28 + 8 = 36 // n = 8
acc = acc + n = 36 + 9 = 45 // n = 9
利用reduce()
,我们可以把求和改成求积,计算求积时,初始值必须设置为1,代码也十分简单:
int s = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9).reduce(1, (acc, n) -> acc * n);
5、输出集合
Stream
的几个常见操作:map()
、filter()
、reduce()
。这些操作对Stream
来说可以分为两类,一类是转换操作,即把一个Stream
转换为另一个Stream
,例如map()
和filter()
,另一类是聚合操作,即对Stream
的每个元素进行计算,得到一个确定的结果,例如reduce()
。
对于Stream
来说,对其进行转换操作并不会触发任何计算!
输出为List
collect(Collectors.toList())
可以把Stream
的每个元素收集到List
中
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);
collect(Collectors.toSet())
可以把Stream
的每个元素收集到Set
中
输出为数组
只需要调用toArray()
方法,并传入数组的“构造方法”:
List<String> list = List.of("Apple", "Banana", "Orange");
String[] array = list.stream().toArray(String[]::new);
collect(Collectors.toSet())
可以把Stream
的每个元素收集到Set
中
输出为Map
要把Stream
的元素收集到Map
中,就稍微麻烦一点。因为对于每个元素,添加到Map
时需要key
和value
,因此,我们要指定两个映射函数,分别把元素映射为key
和value
:
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映射为key:
s -> s.substring(0, s.indexOf(':')),
// 把元素s映射为value:
s -> s.substring(s.indexOf(':') + 1)));
System.out.println(map);
}
}
分组输出
分组输出使用 Collectors.groupingBy()
,它需要提供两个函数:一个是分组的key
,这里使用s -> s.substring(0, 1)
,表示只要首字母相同的String
分到一组,第二个是分组的value
,这里直接使用Collectors.toList()
,表示输出为List
,上述代码运行结果如下:
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);
}
}
输出
{
A=[Apple, Avocado, Apricots],
B=[Banana, Blackberry],
C=[Coconut, Cherry]
}
6、其他操作
排序
对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
的元素进行去重,没必要先转换为Set
,可以直接用distinct()
:
List.of("A", "B", "A", "C", "B", "D")
.stream()
.distinct()
.collect(Collectors.toList()); // [A, B, C, D]
截取
截取操作常用于把一个无限的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
合并为一个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]
flatMap
如果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
:
┌─────────────┬─────────────┬─────────────┐
│┌───┬───┬───┐│┌───┬───┬───┐│┌───┬───┬───┐│
││ 1 │ 2 │ 3 │││ 4 │ 5 │ 6 │││ 7 │ 8 │ 9 ││
│└───┴───┴───┘│└───┴───┴───┘│└───┴───┴───┘│
└─────────────┴─────────────┴─────────────┘
│
│flatMap(List -> Stream)
│
│
▼
┌───┬───┬───┬───┬───┬───┬───┬───┬───┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │
└───┴───┴───┴───┴───┴───┴───┴───┴───┘
并行
通常情况下,对Stream
的元素进行处理是单线程的,即一个一个元素进行处理。但是很多时候,我们希望可以并行处理Stream
的元素,因为在元素数量非常大的情况,并行处理可以大大加快处理速度。
把一个普通Stream
转换为可以并行处理的Stream
非常简单,只需要用parallel()
进行转换:
Stream<String> s = ...
String[] result = s.parallel() // 变成一个可以并行处理的Stream
.sorted() // 可以进行并行排序
.toArray(String[]::new);
经过parallel()
转换后的Stream
只要可能,就会对后续操作进行并行处理。我们不需要编写任何多线程代码就可以享受到并行处理带来的执行效率的提升。
分组
// 将员工按薪资是否高于8000分组
Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
// 将员工按性别分组
Map<String, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getSex));
// 将员工先按性别分组,再按地区分组
Map<String, Map<String, List<Person>>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
接合
joining
可以将 stream
中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。
List<String> list = Arrays.asList("A", "B", "C");
String string = list.stream().collect(Collectors.joining("-"));
// 拼接后的字符串:A-B-C
排序
sorted
,中间操作。有两种排序:
sorted()
:自然排序,流中元素需实现 Comparable
接口sorted(Comparator com)
:Comparator
排序器自定义排序
// 按工资升序排序(自然排序)
List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName)
.collect(Collectors.toList());
// 按工资倒序排序
List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed())
.map(Person::getName).collect(Collectors.toList());
// 先按工资再按年龄升序排序
List<String> newList3 = personList.stream()
.sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName)
.collect(Collectors.toList());
// 先按工资再按年龄自定义排序(降序)
List<String> newList4 = personList.stream().sorted((p1, p2) -> {
if (p1.getSalary() == p2.getSalary()) {
return p2.getAge() - p1.getAge();
} else {
return p2.getSalary() - p1.getSalary();
}
}).map(Person::getName).collect(Collectors.toList());
总结
Stream
提供的常用操作有:
转换操作:map()
,filter()
,sorted()
,distinct()
;
合并操作:concat()
,flatMap()
;
并行处理:parallel()
;
聚合操作:reduce()
,collect()
,count()
,max()
,min()
,sum()
,average()
;
其他操作:allMatch()
, anyMatch()
, forEach()
。
评论 (0)