一文详解 Java Steam

简介: 本次主要介绍Stream引入原因、相关概念以及常用操作


一 为什么引入Stream


首先了解一项技术前,先需要了解为什么需要这项技术以及这项技术主要用来做什么的。在工作中集合对于很多编程任务来说都是非常基本的,但是关于集合上的很多处理方式都是类似于数据库一样的操作,比如匹配某个值,筛选部分元素,对数据进行分组等等,java8之前使用集合处理这些问题就比较麻烦,而且如果集合数据量比较大,为了提高性能,你还需要额外写代码进行并发处理,那就变的更加复杂了。

而引入Stream就是为了让关于集合的操作更加简单,Java 8 中的 Stream 是对集合功能的增强,它允许你以声明性方式处理数据集合。就现在来说,你可以把它们看成遍历数据集的高级迭代器。此外, Stream还可以透明地并行处理,你无需写任何多线程代码,极大的提高编程效率和程序可读性。

比如以下是一个比较经典的对比代码。对集合简单排序的一个demo,以下是java8之前代码:

List<Transaction>groceryTransactions=newArraylist<>();
for(Transactiont: transactions){
if(t.getType() ==Transaction.GROCERY){
groceryTransactions.add(t);
  }
}
Collections.sort(groceryTransactions, newComparator(){
publicintcompare(Transactiont1, Transactiont2){
returnt2.getValue().compareTo(t1.getValue());
  }
});
List<Integer>transactionIds=newArrayList<>();
for(Transactiont: groceryTransactions){
transactionsIds.add(t.getId());
}



而使用Stream之后,上述代码可以直接简化成如下所示的代码:

List<Integer>transactionsIds=transactions.stream()
                .filter(t->t.getType() ==Transaction.GROCERY)
                .sorted(comparing(Transaction::getValue).reversed())
                .map(Transaction::getId)
                .collect(toList());


可以很明显的看出,使用Stream之后的确是极大的提高了编程效率和程序可读性。


二 简介


2.1 Stream定义及基本特征

那么Stream是什么?简短的定义就是“源中支持聚合操作的一系列元素”。让我们分解一下:

  • 元素序列:流为特定元素类型的序列值集提供接口。但是,流实际上并不存储元素。它们是按需计算的。
  • 源:流从提供数据的源(例如集合,数组或I / O资源)进行消耗。
  • 聚合操作:流支持像SQL一样功能的编程语言操作和常用的操作,如filter,map,reduce,find,match,sorted等。

此外,流操作具有两个基本特征:

  • 流水线:许多流操作本身都会返回一个流。这允许将操作链接在一起以形成更大的管道。这使某些优化,如懒惰短路,这是我们后来探索。
  • 内部迭代:与显式迭代的集合(外部迭代)相反,流操作为您在后台进行迭代。


理解上面概念后,我们再回顾下刚才展示的使用Stream代码编写的排序操作,其内部的实现流程大致如下:


2.2  Stream与集合差异

有了基本概念后,我们再来了解下Stream集合的差异,既然Stream是对集合的增强,那两者之间具体有什么区别?可以参考以下官方文档中给出的区别:

1 Sream没有存储空间。流不是存储元素的数据结构。相反,它通过一系列计算操作从数据结构,数组,生成器功能或I / O通道等源中传递元素。

2 Sream本质上是功能性的。对流的操作会产生结果,但不会修改其源。例如,对Stream从集合中获取的Stream进行过滤会产生一个不包含过滤后元素的新元素,而不是从源集合中删除元素。

3 懒惰查找。许多流操作(例如过滤,映射或重复删除)可以延迟实施,从而暴露出优化的机会。例如,“String使用三个连续的元音查找第一个”不需要检查所有输入字符串。流操作分为中间(产生Stream)操作和最终(产生值或副作用)操作。中间操作总是很懒。

4 可以无界。尽管集合的大小是有限的,但流不是必需的。诸如limit(n)或findFirst() 的短路操作可以允许对无限流的计算在有限时间内完成。

5 消耗品。在流的生存期内,流的元素只能访问一次。与 Iterator一样,必须生成新的流以重新访问源中的相同元素。


以上内容主要是oracle官方说明,这里用谷歌翻译了一下,原文可参考:https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

三 Stream 构成


3.1 Stream 分类

对Stream有了一定了解后,我们再来看看Sream的构成,Stream主要分为串行流和并行流,可分别通过stream()和 parallelStream()生成,其中parallelStream()其底层使用Fork/Join框架实现,不需要显示的写并行代码,就可直接对流进行并行操作,非常方便,但也需要注意线程安全问题。详细的可以自己去了解下,这里就不再展开。下面再来了解下Stream的操作类型。

3.2 操作类型

流操作分为中间操作和 终端操作,并合并以形成流管道。流管道由源(例如Collection,数组,生成器函数或I / O通道)组成;随后是零个或多个中间操作,例如Stream.filter或Stream.map;以及诸如Stream.forEach或Stream.reduce的终端操作。

3.2.1 中间操作

中间操作返回一个新的流。他们总是 懒惰; 执行诸如这样的中间操作filter(),实际上并不执行任何过滤,而是创建一个新的流,该新流在遍历时将包含与给定谓词匹配的初始流的元素。在执行管道的终端操作之前,不会开始遍历管道源。中间操作又分为无状态操作 和有状态操作。

无状态操作:在处理新元素时,诸如filter 和等无状态操作map不会保留先前看到的元素的状态-每个元素都可以独立于其他元素上的操作进行处理。

有状态操作:处理元素时会记录状态,例如distinct和sorted在处理新元素时可以合并先前看到的元素的状态。

3.2.2  终端操作

终端操作(例如Stream.forEach或IntStream.sum)可能会遍历流以产生结果或作用。执行终端操作后,流管道被视为已消耗,无法再使用;如果需要再次遍历同一数据源,则必须返回到数据源以获取新的流。终端操作可分为短路操作(如findFirst,allMatch)和非短路操作(如forEach,reduce)。

短路操作:获取到预期结果就会终止。比如anyMatch,findFirst等。

非短路操作:处理完所有的数据才会中止,比如collect,count等。


四 常用API 使用

基本概念介绍完之后,下面开始介绍下怎么使用Stream,以下会从生成方式,常用中间操作以及常用终端操作三部分,介绍下Stream的常用API接口以及使用方式。

4.1 生成方式

1 通过集合Collection的stream()与 parallelStream()方法获取。

List<String>list=newArrayList<>();
//串行流Stream<String>listStream=list.stream();
//并行流Stream<String>listParallelStream=list.parallelStream();


2 数组通过Arrays.stream(Object[]) 或者 Stream.of()生成。

String[] attr= {"1","2","3"};
Stream<String>arrayStream=Arrays.stream(attr);


3 通过类静态工厂方法,如 Stream.of(Object[])]),Stream.generate(Supplier<T> s),IntStream.range(int, int) 或Stream.iterate(Object, naryOperator)。

Stream<String>ofStream=Stream.of(attr);
Stream<Double>generateStream=Stream.generate(Math::random);
IntStreamrangeStream=IntStream.range(1, 2);
Stream<String>iterateStream=Stream.iterate("1", n->n+1);


4 BufferedReader.lines()读取文件的行。

BufferedReaderreader=newBufferedReader(newFileReader("D:\\test_stream.txt"));
Stream<String>bufferedStream=reader.lines();

5 Files.lines() 通过路径读取读取文件的行。

Stream<String>fileStream=Files.lines(Paths.get("D:\\test_stream.txt"), Charset.defaultCharset());


6 随机数流Random.ints()。

Randomrandom=newRandom();
IntStreamrandomStream=random.ints();



7 其他流的生成方法,包括BitSet.stream(),Pattern.splitAsStream(java.lang.CharSequence)和JarFile.stream()。

BitSetbitSet=newBitSet();
IntStreambitSetStream=bitSet.stream();
Patternpattern=Pattern.compile(",");
Stream<String>patternStream=pattern.splitAsStream("1,2,3,4");
JarFilejarFile=newJarFile("D:\\test_stream.txt");
Stream<JarEntry>jarStream=jarFile.stream();

8 其他第三方架包实现。

此外,对于基本数值型,目前可以直接使用三种对应的包装类型 Stream: IntStream、LongStream、DoubleStream


4.2 常用中间操作

4.2.1 常用无状态操作

1 unordered   返回无序的等效流 。可能会返回自身,这是因为流已经无序,或者是因为基础流状态已被修改为无序,主要用来提高并发性能。注意此接口不会每次都返回一个随机排列的无序流。

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().unordered().forEach(System.out::println);


2 filter  过滤流中的某些元素。

//过滤集合中等于2的元素List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().filter("2"::equals).forEach(System.out::println);


3map,mapToInt,mapToLong,mapToDuble

 map: 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。

 mapToInt,mapToLong,mapToDuble: 分别针对不同数据类型的专门map方法,可直接调用特有方法比如sum进行求和等。

//map numbers.stream().map(number->number.concat("2")).forEach(System.out::println);
//mapToIntint[] intArray=numbers.stream().mapToInt(NumberUtils::toInt).toArray();
//mapToLonglong[] longArray=numbers.stream().mapToLong(NumberUtils::toLong).toArray();
//mapToDubledouble[] doubleArray=numbers.stream().mapToDouble(NumberUtils::toDouble).toArray();


4 flatMap ,flatMapToInt,flatMapToLong,flatMapToDuble

 flatMap : 接口接受一个函数作为参数,并将其映射成一个新的元素。由方法生成的各个流会被合并或者扁平化为 一个单一的流

flatMapToInt,flatMapToLong,flatMapToDuble  同map一样 专门针对不同数据类型的特殊处理接口。

这里需要区别下map 和 flatMap。map会直接返回新生成的所有流,而flatMap会做合并或者扁平化所有流,最终返回一个单一流。比如如下代码:

List<String>list1=Arrays.asList("apple", "bag", "led");
List<String>list2=Arrays.asList("toy", "boy", "tree");
//mapSystem.out.println("map:");
Stream.of(list1,list2).map(str->str.stream().map(String::toUpperCase)).forEach(System.out::println);
//flatMapSystem.out.println("flatMap:");
Stream.of(list1,list2).flatMap(str->str.stream().map(String::toUpperCase)).forEach(System.out::println);
输出结果:map:
java.util.stream.ReferencePipeline$3@458ad742java.util.stream.ReferencePipeline$3@5afa04cflatMap:
APPLEBAGLEDTOYBOYTREE

由此可见,对于流组成的列表,map 返回的是流列表中的流,而flatMap是把所有流合并成的一个流,输出流中的内容。


5 peek 遍历每一个元素,返回一个流,只可输出和做些外部处理。

List<String>list1=Arrays.asList("apple", "bag", "led");
list1.stream().map(String::toUpperCase).peek(System.out::println).map(String::toLowerCase).forEach(System.out::println);

这里补充下 peek和map区别,首先看下peek和map接口:

Stream<T>peek(Consumer<?superT>action);
<R>Stream<R>map(Function<?superT, ?extendsR>mapper);

可以看到peek入参是Consumer,没有返回值,而map的入参是Function,可以返回处理后的数据,所以peek一般用来中间操作输出,便于debug 。


4.2.2 常用有状态操作


1 distinct 去除流中的重复元素,注意元素需要实现 hashCode() 和 equals()。

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().distinct().forEach(System.out::println);

2 sorted  排序,有两个方法,默认按照元素预定义的顺序排序进行排序,也可以传入Comparator 自定义排序。

如果没有定义顺序,则不保证每次都是有序的。

list.stream().sorted().forEach(System.out::println);
list.stream().sorted((s1, s2) ->s1.getId().compareTo(s2.getId())).forEach(System.out::println);

3 limit 获取前n个元素,注意如果是个有序且并行的流,则比较影响性能,如果并行流需要使用limit,对顺序又没要求,可以先使用unordered 置为无序的。

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().limit(3).forEach(System.out::println);
numbers.parallelStream().unordered().limit(3).forEach(System.out::println);

4 skip 跳过前n元素,如果n大于元素数量,则返回空流,配合limit 可实现分页,同limit一样,如果并行流需要使用skip ,对顺序又没要求,可以先使用unordered 置为无序的。

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().limit(5).skip(2).forEach(System.out::println);
numbers.parallelStream().unordered().limit(3).forEach(System.out::println);

skip 和 limit其实底层调的是一个方法SliceOps.makeRef,只是传的参数不同


4.3 常用终端操作


4.3.1 常用非短路操作

1 forEach 为流中的每一个元素执行一个动作,并行时不确保执行顺序。

这里说下和peek区别,两者入参都是Consumer,没返回值,但是peek是中间操作,完成后会返回流,继续下一步操作,而forEach 是终端操作,直接结束流。

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().forEach(System.out::println);
numbers.parallelStream().forEach(System.out::println);
//集合还可以简写numbers.forEach(System.out::println);



2 forEachOrdered 按顺序为流中的每一个元素执行一个动作,并行时也可确保执行顺序。

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
numbers.stream().forEachOrdered(System.out::println);
numbers.parallelStream().forEachOrdered(System.out::println);



3 toArray  流转换成数组

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
int[] array=numbers.stream().mapToInt(NumberUtils::toInt).toArray();


4 reduce 归约操作,该操作对每个元素重复应用一个操作(例如,将两个数字相加),直到产生结果为止。在函数式编程中,它通常被称为折叠操作。不少接口比如max,min的底层都是通过调用reduce来实现的。

reduce 有三种接口,下面分别介绍下:

第一种不设置初始值,只传一个有两个参数的函数式接口BinaryOperator,返回一个Optional

//接口定义Optional<T>reduce(BinaryOperator<T>accumulator)
//求和    Optional<Integer>reduce1=Stream.of(2, 4, 6, 7).reduce(Integer::sum);


第二种是设置初始值,再加一个有两个参数的函数式接口BinaryOperator,可以直接返回Stream数据同类型结果,这种操作是从初始值开始计算。

//接口定义Treduce(Tidentity, BinaryOperator<T>accumulator);
//求和Optional<Integer>reduce1=Stream.of(2, 4, 6, 7).reduce(0,Integer::sum);


第三种比较复杂,通常用于并行处理,可以提供一个不同于Stream中数据类型的初始值,通过accumulator计算,最终得到一个U类型的结果返回。然后combiner 再将各个不同线程的结果合并起来,最终得到一个同设置的初始值同类型的结果。

//接口定义<U>Ureduce(Uidentity, BiFunction<U, ?superT, U>accumulator, BinaryOperator<U>combiner);
//求和List<Integer>integers=Arrays.asList(2, 4, 6, 7,Integer.MAX_VALUE);
longreduce2=integers.parallelStream().reduce(0L, (a, b) ->a+b, (a, b) ->a+b);

简单来说,前面两种方式得到的结果,必须与Stream元素类型一致,这样特定情况下就会出现问题,比如不同int相加,可能会超出int范围。但第三种方式的调用也需要注意,如果不是并行流进行处理,第三个参数是一直不会被调用的。

 

5 collect 将java.util.stream .Collector中的各种收集器作为参数, 用于将流的元素累积为汇总结果,这里可以直接用官方提供的Collectors类,里面的提供的方法非常丰富,比如可以把流中的数据汇总成各种集合,求最大值,最小值,求和,分组,求平均数等等

List<Integer>integers=Arrays.asList(2, 4, 6, 7,Integer.MAX_VALUE);
integers.stream().map(a->a+1).collect(Collectors.toList());
integers.stream().map(a->a+1).collect(Collectors.toSet());
integers.stream().map(a->a+1).collect(Collectors.maxBy(Comparator.naturalOrder()));
integers.stream().map(a->a+1).collect(Collectors.minBy(Comparator.naturalOrder()));
//求平均值,分组transactions.stream().collect(groupingBy(Transaction::getCity,groupingBy(Transaction::getCurrency, 
averagingInt(Transaction::getValue))));

当然,你也可以自定义汇总方法,只需要实现java.util.stream .Collector接口就可以,这里就不再展开。


6 max,min

max 返回流中元素最大值

min 返回流中元素最小值

这两个比较简单,其实底层使用的还是reduce

List<Integer>numbers=Arrays.asList(2, 4, 6, 7,8,9);
Optional<Integer>max=numbers.stream().max(Comparator.naturalOrder());        
Optional<Integer>min=numbers.stream().min(Comparator.naturalOrder());
//其实底层调用的是reduceOptional<Integer>max=numbers.stream().reduce(BinaryOperator.maxBy(Comparator.naturalOrder()));
Optional<Integer>min=numbers.stream().reduce(BinaryOperator.minBy(Comparator.naturalOrder()));


7 count 返回流中元素的总个数,这个比较简单,不过底层实现比较有意思,使用的是mapLong

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
longcount=numbers.stream().count();
//count 底层实现numbers.stream().mapToLong(e->1L).sum();


4.3.2 短路操作

1 anyMatch,allMatch,noneMatch

anyMatch 流中有一个元素满足时返回true,否则返回false

allMatch 流中每个元素都符合时才返回true,否则返回false,流为空 返回true

noneMatch 流中每个元素都不符合时才返回true,否则返回false

三个接口底层都是调用MatchOps.makeRef,但有一点需要注意,如果流为空anyMatch 返回false,而allMatch 和 noneMatch 返回true,对此,allMatch的官方解释是此方法评估通用量化,声明流的元素(对于所有x P(x))。如果流为空,称量化为满意,并且始终为true,(与P(x)无关),noneMatch也差不多同样解释。

List<Integer>numbers=Arrays.asList(2, 4, 6, 7,8,9);
booleanisAllMatch=numbers.parallelStream().allMatch(a->a>1);
booleanisAnyMatch=numbers.parallelStream().anyMatch(a->a>3);
booleanisNoneMatch=numbers.parallelStream().noneMatch(a->a>2);
//流设置为空numbers=newArrayList<>();
booleanisAllMatch=numbers.parallelStream().allMatch(a->a>1);
booleanisAnyMatch=numbers.parallelStream().anyMatch(a->a>3);
booleanisNoneMatch=numbers.parallelStream().noneMatch(a->a>2);


2 findFirst,findAny

findFirst 返回流中第一个元素

findAny 返回流中的任意元素

两个接口都是调用FindOps.makeRef,但有点需要注意,如果是串行流,则findAny 会一直返回第一个元素,而不是随机返回

List<String>numbers=Arrays.asList("1", "2", "1", "3", "3", "2", "4");
Optional<Integer>first=numbers.stream().findFirst();
Optional<Integer>any=numbers.stream().findAny();
Optional<Integer>parallelAny=numbers.parallelStream().findAny();


五 总结


本次介绍了Stream引入原因、相关概念以及常用操作,并未太过深入,想要掌握Stream 还是需要日常工作中多使用,多研究。这里再给大家推荐一个idea 插件 java Stream debugger ,可以直接debug看流的内部过程。

最后本文前面概念部分大量参考了官网文档,和官网技术贴,这里贴下原地址,可供参考:

1 https://www.oracle.com/technical-resources/articles/java/ma14-java-se-8-streams.html

2 https://www.oracle.com/java/technologies/architect-streams-pt2.html

3 https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html



相关文章
|
10月前
|
Java
java8中的steam流
java8中的steam流
|
Java
Java steam 中排序的用法
排序的用法
1010 0
|
存储 SQL 安全
【杭州研发中心-后端二团队】Java8新特性之Lambda表达式和Steam流
【杭州研发中心-后端二团队】Java8新特性之Lambda表达式和Steam流
|
2天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在多核处理器日益普及的今天,并发编程成为了软件开发中不可忽视的重要话题。Java语言提供了丰富的并发工具和机制来帮助开发者构建高效且线程安全的应用程序。本文将探讨Java并发的核心概念,包括线程同步、锁机制、以及如何通过这些工具实现性能优化。我们将透过实例分析,揭示并发编程中的常见问题,并展示如何利用现代Java API来解决这些问题。
|
2天前
|
安全 Java 开发者
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】在Java并发编程中,线程安全和性能优化是两个关键要素。本文将深入探讨Java并发编程的基本概念、线程安全的实现方法以及性能优化技巧。通过分析同步机制、锁优化、无锁数据结构和并发工具类的使用,我们将了解如何在保证线程安全的前提下,提高程序的性能。
|
3天前
|
安全 算法 Java
深入理解Java并发编程:线程安全与性能优化
【5月更文挑战第20天】 在Java开发中,正确处理并发问题对于确保应用的稳定性和提高性能至关重要。本文将深入探讨Java并发编程的核心概念——线程安全,以及如何通过各种技术和策略实现它,同时保持甚至提升系统性能。我们将分析并发问题的根源,包括共享资源的竞争条件、死锁以及线程活性问题,并探索解决方案如同步机制、锁优化、无锁数据结构和并发工具类等。文章旨在为开发者提供一个清晰的指南,帮助他们在编写多线程应用时做出明智的决策,确保应用的高效和稳定运行。
|
15小时前
|
Java
【JAVA学习之路 | 提高篇】创建与启动线程之二(继承Thread类)(实现Runnable接口)
【JAVA学习之路 | 提高篇】创建与启动线程之二(继承Thread类)(实现Runnable接口)
|
1天前
|
Java 容器
Java并发编程:深入理解线程池
【5月更文挑战第21天】 在多核处理器的普及下,并发编程成为了提高程序性能的重要手段。Java提供了丰富的并发工具,其中线程池是管理线程资源、提高系统响应速度和吞吐量的关键技术。本文将深入探讨线程池的核心原理、关键参数及其调优策略,并通过实例展示如何高效地使用线程池以优化Java应用的性能。
|
1天前
|
监控 算法 Java
Java并发编程:深入理解线程池
【5月更文挑战第21天】 在现代软件开发中,尤其是Java应用中,并发编程是一个不可忽视的重要领域。合理利用多线程可以显著提高程序的性能和响应速度。本文将深入探讨Java中的线程池机制,包括其工作原理、优势以及如何正确使用线程池来优化应用程序性能。通过分析线程池的核心参数配置,我们将了解如何根据不同的应用场景调整线程池策略,以期达到最佳的并发处理效果。
|
1天前
|
Java 调度 开发者
Java并发编程:深入理解线程池
【5月更文挑战第21天】本文旨在通过深入探讨Java并发编程的核心组件——线程池,为开发者提供对线程池的全面理解。我们将从线程池的基本概念、优势入手,逐步深入到线程池的核心原理、常用配置参数,以及如何合理地使用线程池来提高系统性能和稳定性。文章将结合实际案例,帮助读者掌握线程池的使用技巧,以及在面对不同场景时如何进行调优。