目录
Stream是什么
要回答这个问题,我们先来看一下相关的JavaDoc是怎么描述的:
A sequence of elements supporting sequential and parallel aggregate operations
“支持顺序和并行的聚合操作的元素序列”,乍看之下,感觉是和集合类似的数据结构,存放一组数据,支持某些特定操作,其实不然。先来看一个简单的例子:
// 创建一个流
List<String> list = Arrays.asList("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
list.stream().filter(item -> item.length() < 3) // 过滤掉长度大于等于3的
.distinct() // 去除重复元素
.map(String::toLowerCase) // 对每个元素进行转换
.sorted() // 排序
.forEach(item -> System.out.print(item + " ")); // 遍历每个元素
// 输出结果:a1 a2 b1 b2
从这个例子可以看出,Stream是对集合对象功能的增强,它是有关算法和计算的,专注于对集合对象进行各种便利、高效的操作。借助Lambda表达式,极大的提高了编程效率和代码可读性。
Stream 就如同一个迭代器(Iterator),单向,不可往复。使用Stream对集合中的元素进行操作,感觉就像是灌装啤酒的流水线,一个个空酒瓶子(相当于集合中的元素)被运送到各个操作单元,灌酒/压瓶盖/贴标签/剔除有问题的瓶子(相当于对元素的操作),最后打包装箱堆放到仓库。
怎么创建Stream
常用的创建流的方式有三种。
1.使用Stream的静态方法创建流
// 方式1:
Stream<String> stream1 = Stream.of("A1", "B1", "A2", "B2", "A10", "B10");
stream1.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 B1 A2 B2 A10 B10
// 方式2:
Stream<Integer> stream2 = Stream.iterate(1, (x) -> x + 1).limit(5);
stream2.forEach(item -> System.out.print(item + " "));
// 输出结果:1 2 3 4 5
// 方式3:
Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(item -> System.out.print(item + " "));
// 输出结果:0.8974676207611573 0.7658795436834018 0.48552366426962845
使用iterate和generate比较适合用来方便的构建海量测试数据。使用时要注意配合limit使用,否则会创建出一个无限大的流。
2.使用数组转换
String[] arr = new String[]{"A1", "B1", "A2", "B2", "A10", "B10"};
Stream<String> stream1 = Arrays.stream(arr);
stream1.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 B1 A2 B2 A10 B10
其实在Stream.of()方法的内部也直接调用了Arrays.stream()。
3.使用Collection接口的stream()方法创建
List<String> list = new ArrayList<>();
list.add("A1");
list.add("B1");
Stream<String> stream = list.stream();
stream.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 B1
对于常见的数值集合操作,JDK中额外提供了IntStream,LongStream,DoubleStream三种包装类型的Stream,相当于Stream<Integer>,、Stream<Long>、Stream<Double>,但是减少了额外的boxing 和unboxing操作,提升了效率。
其它的创建方式还有一些,比如:
- java.io.BufferedReader.lines()
- java.util.stream.IntStream.range()
- java.nio.file.Files.walk()
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
对Stream进行操作
当我们使用一个流时,通常包含三个步骤:获取数据源(source) > 数据转换 > 获取最终结果,套用前文中的示例,拆解如下:
Stream的常见操作如下图
- 中间操作 :中间操作的返回结果都是一个新的Stream,意味着一个流后面可以跟随多个中间操作,像stream.a().b().c()……这样开火车。中间操作都是惰性化的(lazy),就是说,仅仅调用到这些方法,并没有真正开始流的遍历。
- 终结操作 :一个流只能有一个终结操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历。
- 无状态 :元素的操作不依赖于其它元素
- 有状态 :元素的操作依赖于其它操作
- 非短路操作 :流中的每个元素都会被处理到
- 短路操作 :只需处理一部分元素就会终止执行。
下面就来为大家演示这些常见操作。演示中如果使用到了自定义的Person类,定义如下:
@Data
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
filter
filter可以用来对流中的元素进行筛选。它对每个元素进行测试运算,保留返回值为true的元素,形成一个新的Stream。
// filter
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.filter(item -> item.length() < 3).forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2
map
对每个元素进行转换,形成一个新的流。
// map
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.map(String::toLowerCase).forEach(item -> System.out.print(item + " "));
// 相当于如下代码:
//stream.map(item -> item.toLowerCase()).forEach(item -> System.out.print(item + " "));
// 输出结果:a1 a1 b1 a2 b2 a10 b10 b10
distinct
去除重复元素。依据元素的equals()方法。
// distinct
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.distinct().forEach(item -> System.out.print(item + " "));
// 输出结果:A1 B1 A2 B2 A10 B10
limit
截取流的前n个元素,n大于流中元素个数时返回所有元素构成的新Stream。
// limit
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.limit(3).forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1
skip
跳过前n个元素,用剩余的元素构成一个新的Stream。n可以大于原始流中的元素个数,此时会得到一个空流。skip可以和limit配合起来实现分页。
// skip
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.skip(2).forEach(item -> System.out.print(item + " "));
// 输出结果:B1 A2 B2 A10 B10 B10
Stream<String> stream2 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream2.skip(8).forEach(item -> System.out.print(item + " "));
// 空流,没有任何输出结果
sorted
对流中的元素进行排序。sorted()方法要求流中的元素实现了Comparable接口,否则会抛出ClassCastException。sorted(Comparator<? super T> comparator)可以指定排序规则。
// sorted
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.sorted().forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 A10 A2 B1 B10 B10 B2
对自定义对象排序示例:
List<Person> list = new ArrayList<>();
for (int i = 3; i > 0; i--) {
list.add(new Person("Name" + i, i * 10));
}
System.out.println(list);
// 输出结果:[Person(name=Name3, age=30), Person(name=Name2, age=20), Person(name=Name1, age=10)]
list.stream().sorted(Comparator.comparingInt(Person::getAge)).forEach(item -> System.out.println(item));
// 输出结果:
// Person(name=Name1, age=10)
// Person(name=Name2, age=20)
// Person(name=Name3, age=30)
forEach
接收一个Lambda表达式,对每一个元素执行该表达式,前面的例子中都使用了它来执行打印。注意forEach是一个终结操作,一旦执行,流就消耗完了,再次操作会抛出异常。forEach的内部实现仍然是传统的for循环,但是节省了很多编码,清爽极了。
// forEach
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
stream.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2 A10 B10 B10
// 再一次调用forEach时会抛出异常 ,因为forEach是终结操作,一旦执行,流就消耗完了。
// java.lang.IllegalStateException: stream has already been operated upon or closed
stream.forEach(item -> System.out.print(item + " "));
peek
和forEach不同,peek是一个中间操作,操作后返回一个新流,可以继续进行操作。
List<Person> list = new ArrayList<>();
for (int i = 3; i > 0; i--) {
list.add(new Person("Name" + i, i * 10));
}
System.out.println(list);
// 输出结果:[Person(name=Name3, age=30), Person(name=Name2, age=20), Person(name=Name1, age=10)]
Stream<Person> stream = list.stream();
stream.peek(item -> item.setName(item.getName().toLowerCase()))
.forEach(item -> item.setAge(item.getAge() + 1));
System.out.println(list);
// 输出结果:[Person(name=name3, age=31), Person(name=name2, age=21), Person(name=name1, age=11)]
reduce
主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average 都是特殊的 reduce
// reduce
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
String str = stream.reduce("", String::concat);
System.out.println(str);
// 输出结果:A1A1B1A2B2A10B10B10
IntStream stream2 = IntStream.of(1, 2, 3, 4, 5);
Integer sum = stream2.reduce(0, (a, b) -> a + b);
System.out.println(sum);
// 输出结果:15
Collect
Collect(收集)是一种是十分有用的最终操作,它可以把stream中的元素转换成另外一种形式。Collect使用Collector作为参数,Java 8内置了各种复杂的收集操作,因此对于大部分常用的操作来说,可以直接使用。
转换为List/Set/Map是最常见的操作了:
// 转换成List
Stream<String> stream = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
List<String> list = stream.filter(item -> item.length() > 2).collect(Collectors.toList());
System.out.println(list);
// 输出:[A10, B10, B10]
// 转换成Set
Stream<String> stream2 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Set<String> set = stream2.collect(Collectors.toSet());
System.out.println(set);
// 输出:[A1, B2, A10, A2, B10, B1]
// 转换成Map
Stream<String> stream3 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Map<String, Integer> map = stream3.distinct().collect(Collectors.toMap(item -> item, String::length));
System.out.println(map);
// {A10=3, B2=2, A1=2, B10=3, A2=2, B1=2}
Collectors.groupingBy可以用来进行分组,也是一个比较常用的功能:
// 按照字符串长度进行分组
Stream<String> stream = Stream.of("A", "A1", "B", "A2", "B1", "A11", "B11", "B11");
Map<Integer,List<String>> map = stream.collect(Collectors.groupingBy(String::length));
System.out.println(map);
// 输出:{1=[A, B], 2=[A1, A2, B1], 3=[A11, B11, B11]}
可以使用Collectors对数据进行统计,计算最大值、最小值、平均值、求和等等,有单项的操作方法,也可用summarizing一次性返回多项统计信息
:
// summingInt求和、averagingInt求平均值 等可以进行单项统计
Stream<String> stream = Stream.of("A", "A1", "A12", "A123", "A1234");
System.out.println(stream.collect(Collectors.summingInt(String::length)));
// 输出:15
// summarizingInt 等可以输出更多的统计信息
Stream<String> stream2 = Stream.of("A", "A1", "A12", "A123", "A1234");
IntSummaryStatistics s = stream2.collect(Collectors.summarizingInt(String::length));
System.out.println(s);
// 输出:IntSummaryStatistics{count=5, sum=15, min=1, average=3.000000, max=5}
前面的例子里,我们曾使用过reduce来将字符串流中的元素连成一个字符串,Collectors也能实现类似功能,
// 直接将元素拼接
Stream<String> stream = Stream.of("A", "A1", "A12", "A123", "A1234");
String s = stream.collect(Collectors.joining());
System.out.println(s);
// 输出:AA1A12A123A1234
// 使用指定的连接字符串、前缀、后缀(可选的)进行拼接,
Stream<String> stream2 = Stream.of("A", "A1", "A12", "A123", "A1234");
String s2 = stream2.collect(Collectors.joining("_","【","】"));
System.out.println(s2);
// 输出:【A_A1_A12_A123_A1234】
match
Stream 有三个 match 方法:
- anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true,否则返回false
- allMatch:Stream 中全部元素符合传入的 predicate,返回 true,否则返回false
- noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true,否则返回false
match属于短路操作,在执行过程中,一旦能够确定最终结果就立即返回。例如anyMatch 只要找到一个符合条件的元素,就立即返回 true,不会再去检测后续的元素。详见下面的示例
// anyMatch
Stream<String> stream = Stream.of("A", "A1", "A12");
boolean b = stream.anyMatch(item -> {
System.out.println("当前元素:" + item);
return item.length() >= 2;
});
System.out.println("最终结果:" + b);
//当前元素:A
//当前元素:A1
//最终结果:true
// allMatch
Stream<String> stream2 = Stream.of("A", "A1", "A12");
boolean b2 = stream2.allMatch(item -> {
System.out.println("当前元素:" + item);
return item.length() >= 2;
});
System.out.println("最终结果:" + b2);
//当前元素:A
//最终结果:false
// noneMatch
Stream<String> stream3 = Stream.of("A", "A1", "A12");
boolean b3 = stream3.noneMatch(item -> {
System.out.println("当前元素:" + item);
return item.length() >= 2;
});
System.out.println("最终结果:" + b3);
//当前元素:A
//当前元素:A1
//最终结果:false
进阶
顺序流,并行流
Stream可以分为顺序流(sequential)和并行流(parallel),前文的例子中使用到的都是顺序流,单线程对流进行处理。并行流使用多线程来处理数据,在大数据量下,可以极大的提升处理的速度。其背后是使用了通用的并发框架 ForkJoinPool ,这是通过利用静态方法 ForkJoinPool.commonPool() 来实现的。对于 ForkJoinPool,其实际使用的线程数取决于机器背后的实际 CPU 核数。
// 我的机器8核CPU
ForkJoinPool commonPool = ForkJoinPool.commonPool();
System.out.println(commonPool.getParallelism());
//输出: 7
可以通过以下JVM参数进行修改:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
可以利用Collection的parallelStream()方法直接创建一个并行流,也可以使用parallel()方法将一个顺序流转为并行流。下面的例子清楚地展示了多个线程参与了并行流的处理。
List<String> list = Arrays.asList("A1", "A2", "A3", "A4", "A5", "A6", "A7", "A8", "A9");
list.parallelStream()
.forEach(item -> System.out.println(Thread.currentThread().getName() + " forEach:" + item));
// 输出:
//main forEach:A6
//main forEach:A5
//ForkJoinPool.commonPool-worker-1 forEach:A3
//ForkJoinPool.commonPool-worker-3 forEach:A4
//ForkJoinPool.commonPool-worker-2 forEach:A8
//ForkJoinPool.commonPool-worker-1 forEach:A1
//ForkJoinPool.commonPool-worker-4 forEach:A7
//ForkJoinPool.commonPool-worker-3 forEach:A9
//main forEach:A2
Stream的API中,有一部分要在并行流下才有用武之地,比如forEachOrdered,作用同forEach,可以遍历元素。主要是作用于并行流时,forEach不能保证遍历元素的顺序,而forEachOrdered可以。
Stream<String> stream1 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Stream<String> stream2 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
Stream<String> stream3 = Stream.of("A1", "A1", "B1", "A2", "B2", "A10", "B10", "B10");
// 顺序流,foreach会按照流中元素的顺序进行遍历
stream1.forEach(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2 A10 B10 B10
// 并行流,可以使用多线程对流进行处理,使用forEach遍历时顺序没有保证
stream2.parallel().forEach(item -> System.out.print(item + " "));
// 输出结果:A10 B2 B10 B10 A2 A1 B1 A1
// 并行流,使用forEachOrdered遍历时顺序有保证
stream3.parallel().forEachOrdered(item -> System.out.print(item + " "));
// 输出结果:A1 A1 B1 A2 B2 A10 B10 B10
惰性的中间操作
前文有提到过中间操作都是惰性化的(lazy),仅仅调用到这些方法,并没有真正开始流的遍历,只有执行了终结操作时,才会开始流的遍历。
Stream<String> stream = Stream.of("A1", "A2", "A3");
stream.peek(System.out::println);
// 这里不会有任何输出,没有调用终结操作,不会执行peek
Stream<String> stream2 = Stream.of("B1", "B2", "B3");
stream2.peek(System.out::println).count();
//输出:
//B1
//B2
//B3
操作的执行顺序
对于形如:stream.filter().map().forEach() 这样的操作,根据代码直观地理解,会对流中的元素进行2次遍历,分别执行filter、map,最后再用forEach进行一次遍历。是这样的吗?我们来看一个例子。
Stream<String> stream = Stream.of("A1", "A2", "B1", "B2");
stream.map(item -> {
System.out.println("map:" + item);
return item.toLowerCase();
}).filter(item -> {
System.out.println("filter:" + item);
return item.startsWith("a");
}).forEach(item -> {
System.out.println("forEach:" + item);
});
//猜想中的输出是先打印4行map,再接着打印4行filter,最后再打印两行foreach
//实际的输出:
//map:A1
//filter:a1
//forEach:a1
//map:A2
//filter:a2
//forEach:a2
//map:B1
//filter:b1
//map:B2
//filter:b2
在这个例子中我们可以看到,处理的顺序是每个元素沿着操作链垂直移动,依次执行所有的操作,然后才是下一个元素。这样在某些场景下可以减少实际的操作执行次数。上面这个例子中总共执行了10次操作,我们来稍微调整一下操作链的顺序,把filter放到map前面:
Stream<String> stream = Stream.of("A1", "A2", "B1", "B2");
stream.filter(item -> {
System.out.println("filter:" + item);
return item.startsWith("A");
}).map(item -> {
System.out.println("map:" + item);
return item.toLowerCase();
}).forEach(item -> {
System.out.println("forEach:" + item);
});
//输出:
//filter:A1
//map:A1
//forEach:a1
//filter:A2
//map:A2
//forEach:a2
//filter:B1
//filter:B2
操作的总次数降为8次。
limit操作为获取前n个元素,skip为跳过n个元素,看起来比较相似,但它们对操作次数的影响有些不同。
Stream<String> stream = Stream.of("A1", "A2", "A3", "A4");
stream.limit(2)
.map(item -> {
System.out.println("map:" + item);
return item.toLowerCase();
}).forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A1
//forEach:a1
//map:A2
//forEach:a2
Stream<String> stream2 = Stream.of("A1", "A2", "A3", "A4");
stream2.map(item -> {
System.out.println("map:" + item);
return item.toLowerCase();
}).limit(2)
.forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A1
//forEach:a1
//map:A2
//forEach:a2
limit无论是在map()前还是后,输出都是一样的,操作执行的次数由limit的参数n决定。再来看一下skip:
Stream<String> stream = Stream.of("A1", "A2", "A3", "A4");
stream.skip(2)
.map(item -> {
System.out.println("map:" + item);
return item.toLowerCase();
}).forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A3
//forEach:a3
//map:A4
//forEach:a4
Stream<String> stream2 = Stream.of("A1", "A2", "A3", "A4");
stream2.map(item -> {
System.out.println("map:" + item);
return item.toLowerCase();
}).skip(2)
.forEach(item -> System.out.println("forEach:" + item));
//输出:
//map:A1
//map:A2
//map:A3
//forEach:a3
//map:A4
//forEach:a4
可以看到skip的位置对最终的操作次数有影响,但无论是limit还是skip,都符合"元素沿着操作链垂直移动"这个逻辑。对于limit、skip有一个特殊的情况,当它们遇到排序操作sorted时:
Stream<String> stream = Stream.of("A1", "A2", "A3", "A4");
stream.sorted((a, b) -> {
System.out.println("当前正在比较:" + a + " 和 " + b);
return a.compareTo(b);
}).limit(2)
.forEach(item -> System.out.println("forEach:" + item));
//输出:
//当前正在比较:A2 和 A1
//当前正在比较:A3 和 A2
//当前正在比较:A4 和 A3
//forEach:A1
//forEach:A2
可以看出,虽然有limit(2)的限定,但是sorted操作还是对所有元素执行了排序。
总结
- Stream是对集合对象功能的增强,专注于对集合对象进行各种便利、高效的操作;
- Stream的操作有中间操作和终结操作,中间操作是惰性的,只有终结操作才会触发中间操作;
- Stream的消费是一次性的,一个stream只能执行一次终结操作;
- 可以使用并行流提升操作的效率
- 合理的设计操作链的顺序可以提升效率