JDK8新特性05:Stream操作
Stream概述
Stream是JDK8提供的一种新的资源管理方式.Stream
不存储值,而是通过管道的方式获取值,因此对流的操作不会修改底层的数据源.
Stream组成
一个完整的流由三部分组成
- 数据源
- 零个或多个中间操作: 中间操作都是惰性求值的,例如执行流的
filter()
方法不会真的对该流进行过滤,而是会返回一个新流,该流在遍历时只会包含原流中通过过滤条件的值. - 一个终止操作: 终止操作是立即求值的,例如
forEach()
方法,会触发对流的遍历.流一旦执行了终止操作,可以认为该流被消耗了(consumed
),不能被再次使用.
Stream的使用示例
-
输出区间
[3,8)
内的所有整数:IntStream.of(3, 4, 5, 6, 7).forEach(System.out::println); IntStream.range(3, 8).forEach(System.out::println); IntStream.rangeClosed(3, 7).forEach(System.out::println);
-
对集合进行
map-reduce
运算:List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6); list.stream().map(num -> num * 2).reduce(0, Integer::sum); // 42
Stream的优势
- Stream操作类似SQL语句是
描述性语言
而非命令式语言
.流操作描述的是结果,而不规定如何实现,由JDK做出优化. - 使用并行流
ParallelStream
,可以方便地执行多线程操作.
Stream的使用
Stream的创建
-
使用
of()
或stream()
方法从集合生成流Stream<String> stream1 = Stream.of("hello", "world", "hello world"); String[] array = new String[]{"hello", "world", "hello world"}; Stream<String> stream2 = Stream.of(array); Stream<String> stream3 = Arrays.stream(array); List<String> list = Arrays.asList("hello", "world", "hello world"); Stream<String> stream4 = list.stream();
-
使用
generate(Supplier supplier)
方法生成无限流:generate()
方法通过调用supplier
对象的get()
方法生成一个无限流. -
使用
iterate(T seed, UnaryOperator<T> function)
方法生成无限流:iterate()
方法通过对种子seed
迭代调用function
对象的apply()
方法,生成一个无限流Stream<Integer> series = Stream.iterate(1, num -> num * 2); // series 为等比数列流{1, 2, 4, 8, ...}
-
使用
concat(Stream stream1, Stream stream2)
拼接流:Stream<String> stream1 = Stream.of("item1", "item2"); Stream<String> stream2 = Stream.of("item3", "item4", "item5"); Stream<String> sumStream = Stream.concat(stream1, stream2); // sumStream 为流{"item1", "item2", "item3", "item4", "item5"}
Stream的中间操作
Stream的所有中间操作都是惰性求值的,并不会立即进行运算,并返回一个新的Stream.
filter()
: 对流进行过滤
filter(Predicate predicate)
方法调用predicate
对象的test()
方法对集合中元素进行过滤.
List<Integer> collect = IntStream.range(3, 8).filter(num -> num > 4).boxed().collect(Collectors.toList());
// 得到 {5, 6, 7}
map()
与flatMap()
:对流中的元素进行映射
map(Function mapper)
是Stream最常用的中间操作,返回一个新的Stream,包含对原流中的每个元素应用mapper
中的apply()
方法得到的结果.
List<String> oldList = Arrays.asList("hello", "world", "hello world");
List<String> newList = oldList.stream().map(String::toUpperCase).collect(Collectors.toList());
// 得到 {"HELLO", "WORLD", "HELLO WORLD"}
flatMap(Function mapper)
可以将二维的Stream拍平成为一维的Stream,其中function
对象的apply()
方法返回的应是Stream
对象.
List<String> greetings = Arrays.asList("hello", "hi", "你好");
List<String> names = Arrays.asList("张三", "李四", "王五");
List<String> results = greetings.stream().flatMap(greeting -> names.stream().map(name -> greeting + " " + name)) // 将二维Stream拍平成一维Stream
.collect(Collectors.toList());
// 得到 {"hello 张三", "hello 李四", "hello 王五", "hi 张三", "hi 李四", "hi 王五", "你好 张三", "你好 李四", "你好 王五"}
skip()
,limit()
:对流进行分页
skip(long n)
方法返回一个新Stream
对象,其中保存的是原Stream
对象中内容除去前n
个之后剩下的内容.若原Stream
对象中保存元素小于n
,则返回一个空的Stream
对象.
limit(long maxSize)
方法返回一个新Stream
对象,其中保存的是原Stream
对象中保存内容的前maxSize
个.
Stream<Integer> stream = Stream.iterate(1, num -> num * 2);
List<Integer> list = stream.skip(3).limit(5).collect(Collectors.toList());
// 得到 {8, 16, 32, 64, 128}
sorted()
:对流排序
sorted()
和sorted(Comparator comparator)
方法返回一个新Stream
对象,其中保存的是原Stream
对象中内容排序后得到的结果.
List<String> unsortedList = Arrays.asList("element3", "element1", "element2");
List<String> sortedList = unsortedList.stream().sorted().collect(Collectors.toList());
// 得到 {"element1", "element2", "element3"}
distinct()
:对流中的元素进行去重
distinct()
方法返回一个新Stream
对象,其保存的是原Stream
对象中的内容去重之后得到的结果.若原流有序,则去重过程是稳定的(重复元素保留第一次出现者);若原流是无序的,则去重操作不保证稳定性.
List<String> duplicateList = Arrays.asList("element1", "element2", "element3", "element1");
List<String> distinctList1 = duplicateList.stream().distinct().collect(Collectors.toList());
// 得到 {"element1", "element2", "element3"}
Stream的终止操作
Stream所有的终止操作都是及早求值的,也就是说会立即进行运算.执行终止操作之后的流不能被复用.
reduce()
:将流中元素聚合为一个结果
reduce()
方法将流中的所有元素聚合为一个结果,有以下三个重载的方法:
-
Optional<T> reduce(BinaryOperator<T> accumulator)
-
T reduce(T identity, BinaryOperator<T> accumulator)
该重载方法得到的结果与流中元素是同类型的,其两个参数意义如下:
identity
: 表示结果的初始值accumlator
: 其apply()
方法表示如何将流中的元素和结果相聚合,应满足结合律
T reduce(T identity, BinaryOperator<T> accumulator)
方法的结果等价于以下代码块,但执行过程未必是顺序的.T result = identity; for (T element : thisStream) result = accumulator.apply(result, element); return result;
-
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
该重载方法得到的结果与流中元素不是同类型的,同时又增加了一个
combiner
参数.combiner
参数仅在并行流中起作用,表示如何将多个并行的流得到的结果汇总.Stream<String> unparallelStream = Stream.of("item1", "item2", "item3", "item4"); Stream<String> parallelStream = Stream.of("item1", "item2", "item3", "item4").parallel(); System.out.println(unparallelStream.reduce("^", String::concat, // null)); // 非并行流,combiner参数不起作用,甚至可以设为null (str1, str2) -> str1 + str2 + "$")); // 得到 "^item1item2item3item4" System.out.println(parallelStream.reduce("^", String::concat, (str1, str2) -> str1 + str2 + "$")); // 得到 "^item1^item2$^item3^item4$$"
collect()
:将流中元素收集到容器中
collect()
方法可以将流中的元素收集到容器中,是一种特化的reduce
操作.有两个重载方法:
-
<R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
其三个参数意义如下:
supplier
: 提供容器的生产者,在并行流中,该生产者的get()
方法会被反复调用,每次都生成一个全新的容器.accumulator
: 其accept()
方法表示如何将流中的元素存入容器.combiner
: 仅在并行流中有效,其accept()
方法表示如何将多个并行的流得到的结果汇总.
下面两个例子演示
collect()
方法的使用// 将流中的元素收集到List中 List<String> List = stringStream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll); // 将流中的元素收集到String中 String concat = stringStream.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
-
<R, A> R collect(Collector<? super T, A, R> collector)
Collector
类将supplier
,accumlator
,combiner
三个参数封装到一个对象中,其帮助类Collectors
提供很多方法获取常用的Collector
对象.stream.collect(Collectors.toList()); // 将流收集为List stream.collect(Collectors.toSet()); // 将流收集为Set stream.collect(Collectors.toCollection(ArrayList::new)); // 将流收集为某种集合 stream.collect(Collectors.joining()); // 将字符串流拼接成字符串
max()
,min()
,average()
,count()
,findAny()
,findFirst()
:见名知义的终止操作
上述终止操作返回的都是Optional
对象,其函数行为可以见名知义.
分组与分区
分组查询Collectors.groupingBy()
Collectors.groupingBy(Function classifier, Collector downstream)
可以对流进行分组查询.返回值为一个Map
,其key
为classifier
参数得到的结果,value
为对组内的元素使用downstream
收集得到的结果.两个参数的意义如下:
classfier
: 分类器,表示返回结果的key
的取值.downstream
: 收集器,对组内元素进行收集后作为返回参数的value
的取值.其默认取值为Collectors.toList()
,表示将分组结果收集到List
中.
Student student1 = new Student("zhangsan", 100, 20);
Student student2 = new Student("lisi", 90, 20);
Student student3 = new Student("wangwu", 90, 30);
Student student4 = new Student("zhangsan", 80, 40);
List<Student> students = Arrays.asList(student1, student2, student3, student4);
Map<String, List<Student>> listMap = students.stream()
.collect(Collectors.groupingBy(Student::getName));
/* 得到
{ lisi=[Student{name='lisi', age=90, score=20}],
zhangsan=[Student{name='zhangsan', age=100, score=20}, Student{name='zhangsan', age=80, score=40}],
wangwu=[Student{name='wangwu', age=90, score=30}]} */
Map<String, Long> longMap = students.stream()
.collect(Collectors.groupingBy(Student::getName, Collectors.counting()));
// 得到 {lisi=1, zhangsan=2, wangwu=1}
分区查询Collectors.partitionBy()
分区查询是分组查询的一种,其分类条件应为一个bool值,只有true
和false
两种情况.
Student student1 = new Student("zhangsan", 100, 20);
Student student2 = new Student("lisi", 90, 20);
Student student3 = new Student("wangwu", 90, 30);
Student student4 = new Student("zhangsan", 80, 40);
List<Student> students = Arrays.asList(student1, student2, student3, student4);
Map<Boolean, List<Student>> listMap = students.stream()
.collect(Collectors.partitioningBy(student -> student.getAge() >= 90));
/* 得到
{ false=[Student{name='zhangsan', age=80, score=40}],
true=[Student{name='zhangsan', age=100, score=20}, Student{name='lisi', age=90, score=20}, Student{name='wangwu', age=90, score=30}]}
*/
Map<Boolean, Double> doubleMap = students.stream()
.collect(Collectors.partitioningBy(student -> student.getAge() >= 90, Collectors.averagingDouble(Student::getScore)));
// 得到 {false=40.0, true=23.333333333333332}
短路操作(short-circuiting
)
一些流操作是短路(short-circuiting
)的(例如limit()
,findFirst()
).对于这些操作,传入无限流,会返回有限流.这些短路操作使得对无限流进行操作和计算成为可能.
下面程序中,findFirst()
方法将流操作短路了:
OptionalInt firstLength = list.stream().mapToInt(str -> {
System.out.println("遍历到 " + str);
return str.length();
}).findFirst();
程序仅输出:
遍历到 hello
但短路操作不是总能发挥短路作用:
List<String> list = Arrays.asList("hello", "world", "helloworld");
OptionalInt firstLength = list.stream().mapToInt(str -> {
System.out.println("遍历到 " + str);
return str.length();
}).sorted().findFirst();
程序输出:
遍历到 hello
遍历到 world
遍历到 helloworld
并行流(parallelStream
)
并行流(parallelStream
)可以使用Collection.parallelStream()
或BaseStream.parallel()
方法生成.
并行流的所有操作及其结果与顺序流是完全相同的,二者的唯一区别在于执行终止操作时,sequentialStream
顺序遍历所有元素,而parallelStream
开启多个线程访问每个元素,其底层是由fork-join
线程池实现的.
int sumOfWeights = widgets.parallelStream()
.filter(b -> b.getColor() == RED)
.mapToInt(b -> b.getWeight())
.sum();
可以使用BaseStream.isParallel()
方法判断当前流是否为并行流,使用BaseStream.sequential()
和BaseStream.parallel()
完成并行流和顺序流之间的转换.
使用Stream时注意的问题
Stream是消耗性的(Consumable
),关闭后不能再被使用
与iterator
类似,Stream是消耗性的资源,只能被使用一次.对Stream进行任何终止操作都会导致流的关闭,使用关闭后的流会抛出IllegalStateException
异常.
IntStream intStream = IntStream.range(1, 11);
System.out.println(intStream.average()); // OptionalDouble[5.5]
System.out.println(intStream.sum()); // java.lang.IllegalStateException: stream has already been operated upon or closed
不应对流执行有副作用(side-effect
)的操作
对于绝大多数流的操作,都不应该执行有副作用(side-effect
)的操作:因为流操作不保证执行顺序;同时有可能带来线程安全问题.只有在少数流操作(如forEach()
和peek()
方法)中,才可以执行带有副作用的操作.
很多带有副作用的操作可以用归约(reduce
)操作来代替,例如下面代码在并行流中会带来线程安全问题:
ArrayList<String> results = new ArrayList<>();
stream.filter(s -> pattern.matcher(s).matches())
.forEach(s -> results.add(s)); // Unnecessary use of side-effects!
上述forEach()
操作会带来线程安全问题,可以用collect()
操作来代替
List<String>results = stream.filter(s -> pattern.matcher(s).matches())
.collect(Collectors.toList()); // No side-effects!
注意无限流产生的问题
对无限流进行终止操作时要注意是否做出了有效的截断,否则会很危险:
// 没做出截断
IntStream.iterate(0, i -> (i + 1) % 2).distinct().forEach(System.out::println);
// 做出无效截断
IntStream.iterate(0, i -> (i + 1) % 2).distinct().limit(6).forEach(System.out::println);
// 做出有效截断
IntStream.iterate(0, i -> (i + 1) % 2).limit(6).distinct().forEach(System.out::println);