Stream
Stream本身是一个接口,里面的方法大部分都是高阶函数。Stream是一个元素序列,支持一些串行和并行的操作。
-
Stream由3部分构成
- 源:这个Stream是来自哪里
- 零个或多个中间操作:对源的操作,会将当前的流转化为另外一个流
- 终止操作
int sum = widgets.stream()
.filter(w -> w.getColor() == RED)
.mapToInt(w -> w.getWeight())
.sum();
-
流操作的分类
- 惰性求值:只有在终止操作被执行时,中间操作才会被执行,例如只有当sum()执行时,中间的filter和mapToInt才会被执行。
- 及早求值:终止操作就是及早求值。
Stream的创建方式
// 第一种
Stream stream1 = Stream.of("a", "b", "c");
// 第二种
String[] myArray = new String[]{"a", "b", "c"};
Stream stream2 = Stream.of(myArray);
Stream stream3 = Arrays.stream(myArray);
// 第三种,通过集合创建Stream对象
List list = Arrays.asList(myArray);
Stream stream4 = list.stream();
Stream带来的简化
Stream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(3, 8).forEach(System.out::println);
for循环与Stream的对比
题设条件:有一个List
for循环
int sum = 0;
for(Integer i : list){
sum+= i * 2;
}
System.out.println(sum);
Stream
System.out.println(list.stream().mapToInt(i -> i * 2).sum());
System.out.println(list.stream().map(i -> i * 2).reduce(0, Integer::sum));
Stream源码
- 流本身并不存储值,而是通过管道来获取值。
- 流本质是函数式的,对流的操作会生成一个结果,不过并不会修改底层的数据源。
- 流的每次操作都会生成一个新的流
通过流来构建数组和List
Stream stream = Stream.of("a", "b", "c");
// lambda表达式方式
String[] stringArray = stream.toArray(i -> new String[i]);
List stringList = stream.collect(Collectors.toList());
// 方法引用方式(构造方法引用)
String[] stringArray2 = stream.toArray(String[]::new);
collect方法
R collect(Supplier supplier,
BiConsumer accumulator,
BiConsumer combiner);
- collect方法是一个终止操作(只要返回的不是Stream,就是终止操作)
- collect方法是一个泛形的,接受3个参数
- collect方法是一个可变的汇聚操作,是通过更新结果的状态进行合并的,而不是通过替换结果来进行合并
collect等价于:
R result = supplier.get();
for (T element : this stream)
accumulator.accept(result, element);
return result;
不使用Collectors.toList,而使用collect方法来组装:
// lambda写法
List stringList2 = stream.collect(() -> new ArrayList<>(),(biList, item) -> biList.add(item),(list1, list2) -> list1.addAll(list2));
// 方法引用写法
List stringList3 = stream.collect(ArrayList::new, ArrayList::add,ArrayList::addAll);
- 第一个Supplier构造了ArrayList集合容器
- 第二个BiConsumer作为
累加器
,每次执行的时候都将创建一个List,将stream中的元素添加到这个集合当中 - 第三个BiConsumer作为
合并器
,语义其实是将第二步获得的每个list,添加到最终返回的List当中
这里谈谈我个人的理解:
- 第一个构造容器,没什么好说的。
- 第二个BiConsumer作为累加器,理论上实现了遍历stream内元素的操作。
- 那么为什么需要第三步来合并呢,应该在第二步add时就可以全部完成了才对。
- 所以我猜测第三个BiConsumer作为合并器,使用场景应该是提供是否支持并行操作使用,如果能支持并行操作,将stream内的元素按照某种规则分配,并行添加完成后,在合并在一起,能提升遍历的效率。
以上的内容为个人的猜想。
Stream实例剖析
Collectors.toCollection
public static >
Collector toCollection(Supplier collectionFactory) {
return new CollectorImpl<>(collectionFactory, Collection::add,
(r1, r2) -> { r1.addAll(r2); return r1; },
CH_ID);
}
toCollection接收一个Supplier函数方法,来构造容器
实际使用场景:
Stream stream = Stream.of("a", "b", "c","c");
List stringList = stream.collect(Collectors.toList());
LinkedList linkedList = stringList.stream()
.collect(Collectors.toCollection(LinkedList::new));
Set set = stringList.stream().collect(Collectors.toCollection(HashSet::new));
FlapMap
与Map差别:
- map : 集合中的元素是什么类型,就返回什么类型的流
- Flapmap:集合流中如果包含了子集合,那么就返回子集合中元素类型的流
Stream> listStream = Stream.of(Arrays.asList("hello1", "world1", "test1")
, Arrays.asList("hello2", "world2", "test3")
, Arrays.asList("hello3", "world3", "test3"));
Stream stringStream = listStream.flatMap(i -> i.stream());
List list = stringStream.collect(Collectors.toList());
list.stream().forEach(System.out::println);
List list = Arrays.asList("hello world","welcome world","hello welcome");
list.stream().map(item -> item.split(" ")).flatMap(i -> Arrays.stream(i)).distinct().collect(Collectors.toList()).forEach(System.out::println);
flapmap将Stream
findFirst
findFirst是一个中断式的方法,返回Stream的第一个元素,以Optional
的形式返回。
Stream stringStream = Stream.gennerate("abc"::new);
Optional optionalS = stringStream.findFirst();
optionalS.ifPresent(System.out::println);
至于为什么findFirst要返回Optional,应该是为了防止空指针异常,有可能Stream内没有元素。
Iterate 与 limt
Stream.iterate(2,i -> i * 2).limit(10).forEach(System.out::println);
- Iterate是一个迭代器,入参第一个为起始值,第二个是一个Function,会生成一个无限迭代的无限流。
- limit是一个中间操作,限制流内元素的个数。
Stream陷阱避免
Stream stream = Stream.iterate(1, i -> i + 2)
.limit(6)
.filter(i -> i > 2);
System.out.println(stream.skip(3));
System.out.println(stream.count());
这段代码将会抛出异常:java.lang.IllegalStateException: stream has already been operated upon or closed
原因是stream每次中间操作,都会返回一个新的stream,并且一个stream只允许被操作一次。
而在执行System.out.println(stream.count());时,stream这个流已经被执行过stream.skip(3)了。
Stream.iterate(1, i -> (i + 1) % 2)
.distinct()
.limit(6)
.forEach(System.out::println);
Stream.iterate(1, i -> (i + 1) % 2)
.limit(6)
.distinct()
.forEach(System.out::println);
第一段段代码将会无限执行下去,而第二段则不会。
原因是limit(6)和 distinct()的顺序不同,distinct后,代码只会返回2个元素,而limit在等待6个元素,所以iterate将无限执行下去,直到能返回6个元素。
流的短路和并发
并行流能提升多少的性能
List list = new ArrayList<>(5000000);
for (int i = 0; i < 5000000; i++) {
list.add(UUID.randomUUID().toString());
}
System.out.println("开始排序");
long start = System.nanoTime();
list.stream().sorted().count();
long end = System.nanoTime();
long use = TimeUnit.NANOSECONDS.toMillis(end - start);
System.out.println("使用时间:" + use);
这里是通过串行流完成500w个uuid排序,在我的电脑上,串行流使用了4秒,而并行流只需要1秒,提升了将近4倍。
Stream是可短路的,是将所有的配置项,例如distinct,sort,map等中间操作,汇总在一起后才执行,并且只要完成要求,后续的元素遍历将不再执行,例如findFirst,获得到符合条件的一个元素后,后续元素将不再得到遍历。
Stream 的分组和分区
分组
Student student1 = new Student("A",100,15);
Student student2 = new Student("B",70,20);
Student student3 = new Student("C",80,25);
Student student4 = new Student("A",59,30);
List list = Arrays.asList(student1,student2,student3,student4);
Map> map = list.stream()
.collect(Collectors.groupingBy(Student::getName));
通过Collectors.groupingBy()
方法,将Stream
如果只需要统计分组后各个依据T.xx的个数,只需要将Collectors.groupingBy(T::xx)
替换为Collectors.groupingBy(T::xx, Collectors.counting())
即可返回Map
分区
- 分组:group by:分组将会依照分组依据来分成多种
- 分区:partition by:而分区则只会分为两种,一种是符合条件,一种是不符合条件(即true和false)。
Map> map = list.stream()
.collect(Collectors.partitioningBy(i -> i.getScore() >= 60));
小结
- Stream没有内部存储,只是通过
操作管道
从source(数据结构,数组,IO等)抓取数据。 - Stream绝不修改自己所封装的底层数据结构的数据,每次操作都返回一个新的Stream
- 如果遇到返回的类型的容器或数组,则可以通过flapmap方法来将其扁平化。
- Stream是支持并行化
- Stream是可以无限的,通过iterate方法可以无限生成。
- Stream是可短路的。