Java 8 Stream使用
Stream简介
我们先来看看Java里面是怎么定义Stream的:
我们来解读一下上面的那句话:
- Stream是元素的集合,这点让Stream看起来用些类似Iterator;
- 可以支持顺序和并行的对原Stream进行汇聚的操作;
Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
Stream有哪些特点:
元素的序列
:与集合一样可以访问里面的元素,集合讲的是数据,而流讲的是操作,比如:filter、map源
: 流也需要又一个提供数据的源,顺序和生成时的顺序一致数据的操作
:流支持类似于数据库的操作,支持顺序或者并行处理数据;上面的例子用流来实现会更加的简洁
1 | public List<Integer> getGt5Data() { |
流水线操作
:很多流的方法本身也会返回一个流,这样可以把多个操作连接起来,形成流水线操作内部迭代
:与以往的迭代不同,流使用的内部迭代,用户只需要专注于数据处理只能遍历一次
: 遍历完成之后我们的流就已经消费完了,再次遍历的话会抛出异常
使用Stream
Java8中的Stream定义了很多方法,基本可以把他们分为两类:中间操作、终端操作;要使用一个流一般都需要三个操作:
- 定义一个数据源
- 定义中间操作形成流水线
- 定义终端操作,执行流水线,生成计算结果
操作符
什么是操作符呢?操作符就是对数据进行的一种处理工作,一道加工程序;就好像工厂的工人对流水线上的产品进行一道加工程序一样。
Stream的操作符大体上分为两种:中间操作符和终止操作符
中间操作符
对于数据流来说,中间操作符在执行制定处理程序后,数据流依然可以传递给下一级的操作符。
中间操作符包含8种
(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):
map
(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B
,这里默认提供了转int,long,double的操作符。flatmap
(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。limit
限流操作,比如数据流中有10个 我只要出前3个就可以使用。distint
去重操作,对重复元素去重,底层使用了equals方法。filter
过滤操作,把不想要的数据过滤。peek
挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。skip
跳过操作,跳过某些元素。sorted
(unordered)排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。
终止操作符
数据经过中间加工操作,就轮到终止操作符上场了;终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。
collect
收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。count
统计操作,统计最终的数据个数。findFirst
、findAny
查找操作,查找第一个、查找任何一个 返回的类型为Optional。noneMatch
、allMatch
、anyMatch
匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。min
、max
最值操作,需要自定义比较器,返回数据流中最大最小的值。reduce
约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。forEach
、forEachOrdered
遍历操作,这里就是对最终的数据进行消费了。toArray
数组操作,将数据流的元素转换成数组。
这里只介绍了Stream,并没有涉及到IntStream、LongStream、DoubleStream,这三个流实现了一些特有的操作符,这里不做介绍。
创建流Stream
Stream.of()
通过Stream类
中的静态方法 of()
1 | Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9); |
Stream.of(array)
1 | Stream<Integer> stream = Stream.of( new Integer[]{1,2,3,4,5,6,7,8,9} ); |
List.stream()
通过Collection 系列集合
提供的串行流:stream()
、并行流: paralleStream()
1 | List<Integer> list = new ArrayList<Integer>(); |
Stream.generate() or Stream.iterate()
使用Stream类的静态方法 iterate 创建无限流
iterate方法:
Stream<T> iterate(final T seed, final UnaryOperator<T> f)
参数 seed 起始值,UnaryOperator 函数式接口 继承Function<T,T> 此时参数类型符合返回值类型一致
1 | Stream<Integer> stream4 = Stream.iterate(0, (x) -> x + 2); |
使用Stream
类的静态方法 generate
创建无限流
generate方法参数为Supplier<T>
供给型接口
1 | Stream<Integer> randomNumbers = Stream |
Stream of String chars or tokens
1 | IntStream stream = "12345_abcdefg".chars(); |
Stream中间操作
中间操作会返回另外一个流,这样可以让多个操作连接起来形成一个流水线的操作,只要不触发终端操作,那么这个中间操作都不会实际执行。
注意点:
- 若只有中间操作,则
不会执行
- 只有终止操作执行后,所有的中间操作
一次执行
,此时就称为延迟加载
或者惰性求值
1 | //取age>30的Stu元素 |
在继续之前,让我们先构建一个List
字符串。我们将在此列表上构建我们的示例,以便于关联和理解。
1 | List<String> memberNames = new ArrayList<>(); |
filter()
该filter()
方法接受一个Predicate来过滤流的所有元素。此操作是中间操作,它使我们能够对结果调用另一个流操作(例如forEach())。
Lambda表达式写法:
1 | memberNames.stream().filter((s) -> s.startsWith("A")) |
非Lambda表达式写法:
1 | memberNames.stream().filter((s) -> { |
程序输出:
1 | Amitabh |
map()
map()
中间操作的流中的每个元素转换成经由给定功能的另一个对象。
以下示例将每个字符串转换为大写字符串。但我们也可以使用map()
将对象转换为另一种类型。
1 | memberNames.stream().filter((s) -> s.startsWith("A")) |
程序输出:
1 | AMITABH |
flatmap()
flatMap()是两步过程,即map() + Flattening。它有助于转换Collection<Collection<T>>
为Collection<T>
- flatmap 作用就是将元素拍平拍扁 ,将拍扁的元素重新组成Stream,并将这些Stream 串行合并成一条Stream
1 | flatMap() 示例 |
sorted()
对Stream的排序通过sorted
进行,它比数组的排序更强之处在于你可以首先对Stream进行各类map、filter、limit、skip甚至distinct来减少元素数量后再排序,这能帮助程序明显缩短执行时间。
该sorted()
方法是返回流的排序视图的中间操作。除非我们传递自定义Comparator ,否则流中的元素按自然顺序排序。
自然排序
1 | List<String> stringList = Arrays.asList("aa", "bb", "dd", "cc","哈哈","啊"); |
程序输出:
1 | aa |
指定排序
1 | sorted(Comparator com) |
- 根据
实现Comparator接口的指定方法
进行排序
1 | stuList.stream().sorted( |
limit()
语法
1 | Stream<T> limit(long maxSize) |
这里maxSize
应该限制流的元素数量;并且方法返回值是一个新的,Stream
由从原始流中挑选的元素组成。
示例 1:Java 程序从无限的偶数流中获取前 10 个偶数
1 | Stream<Integer> evenNumInfiniteStream = Stream.iterate(0, n -> n + 2); |
程序输出。
1 | [0, 2, 4, 6, 8, 10, 12, 14, 16, 18] |
skip()
1 | 方法语法 |
返回一个跳过前n个元素
的流,若流中元素不足n个
,则返回一个空流
。
如果为负,该方法可能会抛出IllegalArgumentExceptionn
。
示例:从无限的偶数流中跳过前 5 个偶数,然后将接下来的 10 个偶数收集到一个新的流中。
1 | Stream<Integer> evenNumInfiniteStream = Stream.iterate(0, n -> n + 2); |
程序输出
1 | [10, 12, 14, 16, 18, 20, 22, 24, 26, 28] |
distinct()
distinct()
去重原理为通过流所生成元素的hashCode()
和equals()
来去除重复
元素
字符串或包装类型去重
很容易从一个字符串集合或包装类型集合中找到重复的元素,因为这些类型实现了equals()
方法
示例:使用 Stream 遍历所有String
元素并使用终端操作将不同的String
元素收集到另一个元素中。List Stream.collect()
1 | // 查找所有不同的字符串 |
程序输出:
1 | [A, B, C, D] |
重写equals()
方法和hashCode()
方法的复杂类型去重
让我们为我们的示例创建一个 Person 类。它有三个领域:id
,fname
和lname
。如果两个人相同,则他们id
是相等的。
1 | public class Person { |
我们测试一下代码。我们创建List
. 然后我们将使用该Stream.distinct()
方法查找具有唯一性的 Person 类的所有实例id
。
1 | //Java 程序通过 id 查找不同的人 |
1 | 输出 |
自定义的复杂类型去重
有时,我们希望根据自定义逻辑找到不同的项目。
例如,我们可能需要找到所有可能有不同id
但全名相同的人。在这种情况下,我们必须根据Person
类fname
和lname
字段检查相等性。
Java 没有任何用于在使用提供的用户函数比较对象时查找不同对象的本机 API。因此,我们将创建自己的实用程序函数,然后使用它。
distinctByKey()
该distinctByKey()
函数使用一个ConcurrentHashMap
实例来确定是否存在具有相同值的现有键——其中键是从函数引用中获取的。
此函数的参数是一个lambda 表达式,用于生成映射键以进行比较。
1 | 按类字段查找不同的实用函数public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) |
我们可以将任何字段 getter 方法作为方法参数传递,这将导致字段值充当映射的键。
示例:
检查我们distinctByKey(p -> p.getFname() + " " + p.getLname())
在filter()
方法中的使用方式。
1 | Java 程序按姓名查找不同的人Person lokeshOne = new Person(1, "Lokesh", "Gupta"); |
程序输出:
1 | 输出[ |
peek()
peek
:如同于map,能得到流中的每一个元素。但map接收的是一个Function表达式,有返回值;而peek接收的是Consumer表达式,没有返回值。
peek()
存在主要是为了支持调试
我们看下debug用途的使用:
1 | Stream.of("one", "two", "three","four").filter(e -> e.length() > 3) |
上面的例子输出:
1 | Filtered value: three |
上面的例子我们输出了stream的中间值,方便我们的调试。
为什么只作为debug使用呢?我们再看一个例子:
1 | Stream.of("one", "two", "three","four").peek(u -> u.toUpperCase()).forEach(System.out::println); |
上面的例子我们使用peek将element转换成为upper case。然后输出:
1 | one |
可以看到stream中的元素并没有被转换成大写格式。
再看一个map的对比:
1 | Stream.of("one", "two", "three","four").map(u -> u.toUpperCase()).forEach(System.out::println); |
输出:
1 | ONE |
可以看到map是真正的对元素进行了转换。
Stream终止操作
foreach()
Stream forEach(Consumer action)
对流的每个元素执行一个操作。Stream forEach(Consumer action) 是一个终端操作,即它可以遍历流以产生结果或副作用。
1 | memberNames.forEach(System.out::println); |
方法语法
该forEach()
方法的语法如下:
1 | void forEach(Consumer<? super T> action) |
Consumer
是一个功能接口,action
表示要对 Stream 中的每个元素执行的非干扰操作。
特点:
- 该
forEach()
方法是终端操作。这意味着它不再返回Stream
。 - 执行
forEach()
后,认为流管道已消耗,不能再使用 Stream。 - 如果我们需要再次遍历相同的数据源(支持 Stream 的集合),我们必须返回数据源以获取新的流。
- 对于并行流,该
forEach()
操作不保证流中元素的顺序,因为这样做会牺牲并行性的好处。 - 使用 Lambda 运算符:在stream().forEach() 中,使用了 lambda,因此不允许对循环外的变量进行操作。只能对相关集合进行操作。
1 | List<String> arr1 = new ArrayList<String>(); |
示例:
1 | list.stream().forEach( System.out::println ); |
forEachOrdered()
同forEach()
- forEachOrdered 适用用于并行流的情况下进行迭代,能保证迭代的有序性
示例:
1 | List<Integer> list = Arrays.asList(2, 4, 6, 8, 10); |
In the above example, both statements guarantee that output will be 2, 4, 6, 8, 10.
1 | //forEach() |
toArray()
该toArray()
方法返回一个包含给定流元素的数组。这是终端操作。
示例 :将字符串流转换为字符串数组
1 | 将字符串流转换为数组 |
程序输出
1 | [A, B, C, D] |
reduce()
很多时候,我们需要执行将流归约为单个结果值的操作,例如,最大值、最小值、总和、乘积等。reduce是组合所有元素的重复过程。
reduce操作将二元运算符应用于流中的每个元素,其中运算符的第一个参数是前一个reduce操作的返回值,第二个参数是当前流元素。
Optional<T> reduce(BinaryOperator<T> accumulator)
:第一次执行时,accumulator函数的第一个参数为流中的第一个元素,第二个参数为流中元素的第二个元素;第二次执行时,第一个参数为第一次函数执行的结果,第二个参数为流中的第三个元素;依次类推。
示例 1:获取最长的字符串
1 | List<String> words = Arrays.asList("GFG", "Geeks", "for","GeeksQuiz", "GeeksforGeeks"); |
示例 3:求和
1 | List<Integer> array = Arrays.asList(-2, 0, 4, 6, 8); |
collect()
collect() 流式传输到 List
collect()
方法用于从stream
接收元素并将它们存储在集合中。
1 | List<String> memNamesInUppercase = memberNames.stream().sorted() |
程序输出:
1 | [AMAN, AMITABH, LOKESH, RAHUL, SALMAN, SHAHRUKH, SHEKHAR, YANA] |
collect() 流式传输到 Set
我们可以使用Collectors.toSet()
将流元素收集到一个新的 Set 中。
1 | List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); |
collect() 流式传输到 Map
我们可以使用Collectors.toMap()
函数将流元素收集到Map
。此方法接受映射键的两个参数和 Map 中的相应值。
1 | List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6); |
Collectors joining()
我们可以使用 Collectors join() 方法来获取一个 Collector,它将输入流 CharSequence 元素按照遇到的顺序连接起来。我们可以使用它来连接字符串流、StringBuffer或 StringBuilder。
1 | String value = Stream.of("a", "b", "c").collect(Collectors.joining()); |
Collectors类中的常用方法
counting
-统计数量averagingDouble
-求平均值并转换成Double类型averagingInt
-求平均值并转换成Int类型averagingLong
-求平均值并转换成Long类型summingDouble
-求和并转换成Double类型summingInt
-求和并转换成Int类型summingLong
-求和并转换成Long类型maxBy
-根据函数条件求最大值groupingBy
-分组partitioningBy
-分区joining
-连接字符串
示例 求年龄的平均值
1 | Double ageAve = stuList.stream() |
求年龄之和
1 | Double ageSum = stuList.stream() |
根据年龄找出最大年龄值的stu对象
1 | //根据年龄找出最大年龄值的stu对象 |
min()
返回流中的最小值
这是终端操作。所以这个方法执行后就不能使用stream了。
根据提供的Comparator回此流的最小元素。
这是流缩减的一个特例,缩减到只有一个元素。
该方法返回一个描述此流的最小元素的Optional,
Optional
如果流为空,则返回一个空。
示例:查找最小数字
1 | List<Integer> list = Arrays.asList(2, 4, 1, 3, 7, 5, 9, 6, 8); |
程序输出。
1 | 1 |
max()
返回流中的最大值
这是终端操作。所以这个方法执行后就不能使用stream了。
根据提供的Comparator回此流的最大元素。
这是流缩减的一个特例,缩减到只有一个元素。
该方法返回一个描述此流的最大元素的Optional,
Optional
如果流为空,则返回一个空。
示例:查找最大数字
1 | List<Integer> list = Arrays.asList(2, 4, 1, 3, 7, 5, 9, 6, 8); |
程序输出。
1 | 9 |
count()
返回流中元素的总个数
示例 1:计算 List 中元素个数
1 | long count = Stream.of("how","to","do","in","java").count(); |
示例 2:使用 Collectors.counting() 计算元素数量
1 | long count = Stream.of("how","to","do","in","java").collect(Collectors.counting()); |
anyMatch()
接收一个 Predicate 函数,只要流中有一个元素满足该断言则返回true,否则返回false
- 这是一个短路终端操作。
- 它返回此流的任何元素是否与提供的谓词匹配。
- 如果不需要确定结果,则可能不会评估所有元素的谓词。
true
一旦遇到第一个匹配元素,方法就会返回。 - 如果流为空,则
false
返回并且不评估谓词。 - allMatch() 和 anyMatch() 之间的区别在于,如果流中的任何元素与给定的谓词匹配,则
anyMatch()
返回true
。使用 时allMatch()
,所有元素必须匹配给定的谓词。
示例 1:检查流是否包含特定元素
1 | Stream<String> stream = Stream.of("one", "two", "three", "four"); |
anyMatch() 与 contains() 的区别
对于集合List来说,anyMatch()
和contains()没有区别
anyMatch()
在某些需要检查流中是否至少有一个元素的情况下,该方法很有用。
较短的版本list.contains()
也做同样的事情,可以代替使用。
allMatch()
allMatch
-检查是否匹配所有
元素
示例 1:Stream.allMatch()
检查所有流元素是否不包含任何数字字符
1 | Stream<String> stream = Stream.of("one", "two", "three", "four"); |
noneMatch()
句法
1 | boolean noneMatch(Predicate<? super T> predicate) |
的noneMatch()
回报:
true – 如果流中没有元素匹配给定的谓词,或者流为空。
false – 如果至少一个元素与给定的谓词匹配。
它与方法allMatch() 完全相反。
示例 1:Stream.noneMatch()
检查 Stream 中的元素是否包含任何数字/数字字符
1 | Stream<String> stream = Stream.of("one", "two", "three", "four"); |
findFirst()
1 | Optional<T> findFirst() |
此方法返回一个Optional描述此流的第一个匹配元素。
1 | Optional<Customer> optional = allCustomers.stream() |
findAny()
findAny
-返回当前流中的任意一个元素
1 | //从集合中随便找个age>30的Stu对象 可以使用串行流stream,也可以使用parallelStream 并行流 |
findFirst() 与 findAny()
在非并行流中,大多数情况下两者都可能返回流的第一个元素,但不提供此行为的任何保证。findAny()
用于findAny()
在更快的时间内从任何并行流中获取任何元素。否则,我们总是可以findFirst()
在大多数情况下使用。
Stream使用示例
示例1:查找流的最后一个元素
Stream.reduce() API
该reduce()
方法对流的元素使用归约技术。在这种情况下,它将选择流的两个元素并选择后者。这将一直持续到所有元素都用尽为止。
在归约过程结束时,我们将得到流的最后一个元素。
1 | // 使用 reduce() 方法获取最后一个元素 |
如果流本身是空的,则lastElement为-1。
示例2:查找和计算重复项并删除这些重复项
Stream.distinct() - 删除重复项
删除重复的字符串
distinct()方法根据对象的equals()
方法检查对象是否相等
1 | List<String> list = Arrays.asList("A", "B", "C", "D", "A", "B", "C"); |
删除重复的自定义对象
可以使用相同的语法从List 中删除重复的对象。为此,我们需要非常小心对象的equals()
和hashCode()
方法,因为它将决定对象是重复的还是唯一的。
考虑下面的例子,如果两个Person实例具有相同的id值,则它们被认为是相等的。
1 | public class Person |
删除重复的Person对象
1 | Person p1 = new Person(1,"zhang","san"); |
Collectors.toSet() - 删除重复项
另一种简单且非常有用的方法是将所有元素存储在Set
. 根据定义,集合仅存储不同的元素。请注意,Set通过使用*equals()*方法比较对象来存储不同的项目。
1 | ArrayList<Integer> numbersList |
程序输出:
1 | [1, 2, 3, 4, 5, 6, 7, 8] |
Collectors.toMap() - 计算重复项
有时,我们有兴趣找出哪些元素是重复的,以及它们在原始列表中出现的次数。我们可以使用 一个Map
来存储这些信息。
我们必须遍历列表,将元素作为 Map 键,并将其所有出现在 Map 值中。
1 | // ArrayList with duplicate elements |
程序输出:
1 | {1=2, 2=1, 3=3, 4=1, 5=1, 6=3, 7=1, 8=1} |
去重
1 | Person p1 = new Person(1,"zhang","san"); |
示例3: 图解Stream处理过程
1 | List<Integer> transactionsIds = |