平时工作中,我在处理集合的时候,总是会用到各种流操作,但是往往在处理一些较为复杂的集合时,还是会出现无法灵活运用api的场景,这篇文章的目的,主要是为介绍一些工作中使用流时的常用操作,例如去重、排序和数学运算等内容,并不对流的原理和各种高级api做深度剖析,让我们开始吧~

如果读者你已经对流有一些基本的了解,现在只是有些场景运用到流,不知道如何使用,请划到文章的最后一个部分-常用操作,希望能够帮助到你。^^

一、流的组成

往往我们使用流的时候,都会经过3步,如下图所示,首先我们创建一个流,然后对流进行一系列的中间操作,最后执行一个终端操作,这个流就到此结束了。

  1. 创建流:有且创建一次即可。

  2. 中间操作:0个,1个及多个均可,可以进行链式操作。

  3. 终端操作:一条语句中有且只存在1个,一旦进行该操作,代表该流已结束。

我们需要关注的,实际上是对流的中间操作和终端操作。

二、举例对象

例子:现在我们多个用户,抽象成List<User>,该用户有ID,名称,年龄,钱以及拥有多个账户。

@Data
public class User{
   private Integer id;
   private String name;
   private int age;
   private BigDecimal money;
   private List<Account> accounts;
}

// 操作
List<User> users = new ArrayList<>();

三、创建流

3.1 Collection集合

串行流线程安全,保证顺序;并行流线程不安全,不保证顺序,但是快。

// 串行流
Stream<User> stream = users.stream();
// 并行流
Stream<User> stream = users.parallelStream();

3.2 数组

Stream.of()方法底层仍然用得是Arrays.stream()。

String[] userNameArray = {"mary", "jack", "tom"};

// 方法1
Stream<String> stream = Arrays.stream(userNameArray);
// 方法2
Stream<String> stream = Stream.of(userNameArray);

3.3 多个元素

Stream.of()方法可接收可变参数,T... values。

Stream<String> stream = Stream.of("mary", "jack", "tom");

3.4 特殊类型流

处理原始类型int、double、long

IntStream intStream = IntStream.of(1, 2, 3);

四、中间操作

4.1 映射和消费

map():可将集合中的元素映射成其他元素。例如 List<User> -> List<String>

flatmap():将映射后的元素放入新的流中,可将集合中元素的某个集合属性扁平化。例如List<List<Account>> -> List<Account>

peek:对集合中的元素进行一些操作,不映射。例如List<User> -> List<User>

// map 
List<String> userNames = users.stream().map(User::getName).collect(Collectors.toList());
// flatmap
List<Account> accounts = users.stream().map(User::getAccounts).flatMap(Collection::stream).collect(Collectors.toList());
// peek
List<User> newUsers = users.stream().peek(user -> user.setName("Jane")).collect(Collectors.toList());

4.2 过滤和去重

filter():保留符合条件的所有元素。

distinct():根据hashCode()和equals方法进行去重。

skip(n):跳过前n个元素。

limit(n):获取前n个元素

// filter(常用)
List<User> newUsers = users.stream().filter(user -> user.getAge() > 15).collect(Collectors.toList());
// distinct
List<User> newUsers = users.stream().distinct().collect(Collectors.toList());
// limit
List<User> newUsers = users.stream().skip(2).collect(Collectors.toList());
// skip
List<User> newUsers = users.stream().limit(2).collect(Collectors.toList());

五、终端操作

5.1 收集

5.1.1 collect()

collect():将流中的元素收集成新的对象,例如List, Set, Map等,这个方法有两种参数,我们常用的是第一种,利用Collectors工具类来获取Collector对象,第二种在实际工作中用得少,本文便不介绍,读者有兴趣可去自行了解。:p

  • collect(Collector):(常用)

  • collect(Supplier, BiConsumer, BiConsumer)

收集
// list
List<User> newUsers = users.stream().collect(Collectors.toList());
// set
Set<User> newUsers = users.stream().collect(Collectors.toSet());
// map
// toMap():
// 第一个参数是map的key;
// 第二个参数是map的value(Function.identity()代表取自身的值);
// 第三个参数是key相同时的操作(本行代表key相同时,后面的value覆盖前面的value)
Map<Integer, User> map = users.stream().collect(Collectors.toMap(User::getId, Function.identity(), (v1, v2) -> v1));
分组
// 根据对象中某个字段分组
Map<Integer, List<User>> map = users.stream().collect(Collectors.groupingBy(User::getId));
// 根据对象中某个字段分组后,再根据另外一个字段分组
Map<Integer, Map<String, List<User>>> map = users.stream().collect(Collectors.groupingBy(User::getId, Collectors.groupingBy(User::getName)));
拼接
// 拼接,比如"hello", "world" -> "hello,world"
String str = users.stream().map(User::getName).collect(Collectors.joining(","));

5.1.2 toArray()

toArray():将List的流收集成数组Array。

// 可利用String[]::new来指定类型
String[] userNames = users.stream().map(User::getName).toArray(String[]::new);

5.2 断言

allMatch():所有元素符合条件则返回true,否则返回false。 noneMatch():所有元素都不符合条件则返回true,否则返回false。 anyMatch():存在元素符合条件则返回true,否则返回false。

// 是否所有的用户年龄都大于15
boolean allMatch = users.stream().allMatch(user -> user.getAge() > 15);
// 是否所有的用户年龄都不大于15
boolean noneMatch = users.stream().noneMatch(user -> user.getAge() > 15);
// 是否存在用户年龄大于15
boolean anyMatch = users.stream().anyMatch(user -> user.getAge() > 15);

5.3 规约

reduce():可以将流的元素组合成一个新的结果。

这个API,我在实际工作中用得很少……可能在计算BigDecimal之和的时候才会用到: BigDecimal sum = users.stream().map(User::getMoney).reduce(Bigdecimal.ZERO, BigDecimal::add);

// 指定初始值:
// 相当于new User(1 + users中所有的ID之和,"1", 0, 0)
User user1 = users.stream().reduce(new User(1, "1", 0, 0), (u1, u2) -> {
   u1.setId(u1.getId() + u2.getId());
   return u1;
});
// 不指定初始值:
// 相当于new User(users中所有的ID之和,"1", 0, 0)
User user2 = users.stream().reduce((u1, u2) -> {
   u1.setId(u1.getId() + u2.getId());
   return u1;
}).orElse(null);

5.4 过滤

findAny():返回流中任意一个元素,如果流为空,返回空的Optional。

findFirst():返回流中第一个元素,如果流为空,返回空的Optional。

并行流,findAny会更快,但是可能每次返回结果不一样。

// findAny()
Optional<User> optional = users.stream().findAny();
// findFirst
Optional<User> optional = users.stream().findFirst();

// 建议先用isPresent判空,再get。
User user = optional.get();

六、常用操作

6.1 扁平化

我们想要换取 所有用户 的 所有账号 ,比如List<Account>,可以使用flatMap来实现。

两种方法获取结果一模一样。

// 方法1:
List<Account> accounts = users.stream()
      .flatMap(user -> user.getAccounts().stream())
      .collect(Collectors.toList());
// 方法2:
List<Account> accounts = users.stream()
      .map(User::getAccounts)
      .flatMap(Collection::stream)
      .collect(Collectors.toList());

6.2 流的逻辑复用

实际工作中,我们可能存在对一个集合多次中间操作后,经过不同的终端操作产生不同的结果这一需求。这个时候,我们就产生想要流能够复用的想法,但是实际上当一个流调用终端操作后,该流就会被关闭,如果关闭后我们再一次调用终端操作,则会产生stream has already been operated upon or closed这个Exception,我们无奈之下,只好把相同的逻辑,重复再写一遍……

如果想使得流逻辑复用,我们可以用Supplier接口把流包装起来,这样就可以实现啦。

不过要注意一点,并不是流复用,而是产生流的逻辑复用,其实还是生成了多个流。

比如我们想要15岁以上的:(1)所有用户集合;(2)根据ID分组后的集合。

// 1. 复用的逻辑
Supplier<Stream<User>> supplier = () -> users.stream().filter(user -> user.getAge() > 15);

// 2.1 所有用户集合
List<User> list = supplier.get().collect(Collectors.toList());
// 2.2 根据ID分组后的集合
Map<Integer, List<User>> map = supplier.get().collect(Collectors.groupingBy(User::getId));

6.3 排序

根据基础类型和String类型排序:

比如List<Integer>List<String>集合,可使用sorted()排序, 默认升序。

注意:例如"123",字符串类型的数字不可直接比较,因为它是根据ASCII码值来比较排序的。

// 升序 {3, 2, 4} -> {2, 3, 4}
List<Integer> newList = list.stream().sorted().collect(Collectors.toList());
// 降序 {3, 2, 4} -> {4, 2, 3}
List<Integer> newList = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());

根据对象中某个字段排序:

根据ID进行排序。

// 升序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId)).collect(Collectors.toList());
// 降序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId).reversed()).collect(Collectors.toList());
// 先根据ID排序,再根据age排序
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId).thenComparing(User::getAge)).collect(Collectors.toList());

其中User可能为null,User中的ID也可能为null。

  • 方法1:先过滤,再排序

  • 方法2:可使用nullFirst或者nullLast

// 2.1 如果User可能为null
List<User> newUsers = users.stream().sorted(Comparator.nullsLast(Comparator.comparing(User::getId))).collect(Collectors.toList());
// 2.2 如果User中的ID可能为null
List<User> newUsers = users.stream().sorted(Comparator.comparing(User::getId, Comparator.nullsLast(Comparator.naturalOrder()))).collect(Collectors.toList());

6.4 去重

根据基础类型和String类型去重:

比如List<Integer>List<String>集合,可使用distinct()去重。

List<Integer> newList = list.stream().distinct().collect(Collectors.toList());

根据对象中某个或多个字段去重:

ID有可能相同,根据ID进行去重。

// 方法一:使用TreeSet去重,但是这个方法有副作用,会根据ID排序(TreeSet特性)
List<User> newUsers = users.stream().collect(Collectors.collectingAndThen(
             Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId))), ArrayList::new));

// 方法二:使用Map的key不可重复的特性,进行去重
List<User> newUsers = users.stream().collect(Collectors.toMap(User::getId, b -> b, (b1, b2) -> b2))
              .values().stream().collect(Collectors.toList());

// 方法三:自定义方法去重
List<User> newUsers = users.stream().filter(distinctByKey(User::getId)).collect(Collectors.toList());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {
       Map<Object,Boolean> seen = new ConcurrentHashMap<>();
       return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

根据ID和Age两个字段进行去重。

List<User> newUsers = users.stream().filter(distinctByKey(User::getId, User::getAge)).collect(Collectors.toList());
private static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor1, Function<? super T, ?> keyExtractor2) {
   Map<Object,Boolean> seen = new ConcurrentHashMap<>();
   return t -> seen.putIfAbsent(keyExtractor1.apply(t).toString() + keyExtractor2.apply(t).toString(), Boolean.TRUE) == null;
}

其中User可能为null,User中的ID也可能为null(参考排序)。

// 如果User中的ID可能为null:可使用nullFirst或者nullLast
List<User> newUsers = users.stream().collect(Collectors.collectingAndThen(
               Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(User::getId,
                       Comparator.nullsFirst(Comparator.naturalOrder())))), ArrayList::new));

6.5 数学运算

计算平均值:

// 方法1:mapToInt会将当前流转换成IntStream
double average = users.stream().mapToInt(User::getAge).average().getAsDouble()
double average = users.stream().mapToInt(User::getAge).summaryStatistics().getAverage();
// 方法2:Collectors实现的平均数
double average = users.stream().collect(Collectors.averagingInt(User::getAge));

计算总和:

// BigDecimal
BigDecimal sum = users.stream().map(User::getMoney).reduce(Bigdecimal.ZERO, BigDecimal::add);
// int、double、long:
int sum = users.stream.mapToInt(User::getNum).sum;

计算最大值:

找到年龄最大的用户。

int age = users.stream().max(Comparator.comparing(User::getAge)).orElse(null);

计算最小值:

找到年龄最小的用户。

int age = users.stream().min(Comparator.comparing(User::getAge)).orElse(null);

七、结尾

关于流的一些常用操作就介绍完啦~希望大家能有所收获。我是宋影,第一篇技术类博文就此奉上啦。

参考博文:

  1. https://juejin.cn/post/6844903830254010381#heading-9

  2. https://blog.csdn.net/sinat_36184075/article/details/111767670

  3. https://colobu.com/2016/03/02/Java-Stream/

  4. http://www.itwanger.com/life/2020/04/01/java-stream.html

Stream流的基本介绍以及在工作中的常用操作(去重、排序以及数学运算等)的更多相关文章

  1. git工作中的常用操作

    上班开始,打开电脑,git pull:拉取git上最新的代码: 编辑代码,准备提交时,git stash:将自己编辑的代码暂存起来,防止git pull时与库中的代码起冲突,否则自己的代码就白敲了: ...

  2. 工作中oracle常用操作

    常用数据库操作 启动数据库监听器lsnrctl start 停止数据库监听器lsnrctl stop 登录oraclesqlplus / as sysdba启动oralcestartup;关闭orac ...

  3. 使用传统的方式遍历集合对集合中的数据进行过滤和使用Stream流的方式遍历集合对集合中的数据进行过滤

    使用传统的方式,遍历集合,对集合中的数据进行过滤 class Test{ public static void main(String[] args){ ArrayList<String> ...

  4. 转://工作中 Oracle 常用数据字典集锦

    DBA工作中数据字典就等同于我们本和笔,时时刻刻也分不开的,不管是看状态,还是监控,都需要数据字典的支持,本文整理出来常用的数据字典系列,帮助大家来记住和汇总以便查询利用 ALL_CATALOG Al ...

  5. 【mysql】工作中mysql常用命令及语句

    1.查看mysql版本号 MySQL [release_test_oa]> select version(); +------------+ | version() | +----------- ...

  6. 工作中经常用到github上优秀、实用、轻量级、无依赖的插件和库

    原文收录在我的 GitHub博客 (https://github.com/jawil/blog) ,喜欢的可以关注最新动态,大家一起多交流学习,共同进步,以学习者的身份写博客,记录点滴. 按照格式推荐 ...

  7. 工作中一些常用的linux命令

    问题一: 绝对路径用什么符号表示?当前目录.上层目录用什么表示?主目录用什么表示? 切换目录用什么命令? 答案:绝对路径:如/etc/init.d当前目录和上层目录:./  ../主目录:~/切换目录 ...

  8. git工作中最常用的用法教程,不走命令行

    ·1.1 git的概述 Git(读音为/gɪt/.)是一个开源的分布式版本控制系统,可以有效.高速的处理从很小到非常大的项目版本管理.  Git 是 Linus Torvalds 为了帮助管理 Lin ...

  9. 工作中最常用的Excel函数公式大全

    电脑那些事儿2016-05-18 22:23:02微软 公式 工作阅读(22574)评论(1) 声明:本文由入驻搜狐公众平台的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场.举报 Wo ...

随机推荐

  1. 【LeetCode】274. H-Index 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/h-index/ ...

  2. 【LeetCode】150. Evaluate Reverse Polish Notation 解题报告(Python)

    [LeetCode]150. Evaluate Reverse Polish Notation 解题报告(Python) 标签: LeetCode 题目地址:https://leetcode.com/ ...

  3. Mysterious For(hdu4373)

    Mysterious For Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)To ...

  4. 亲测:三个值得练手的Java实战项目

    测试奇谭,BUG不见. 大家好,我是谭叔. 一提到编码,很多小伙伴便感到头疼,特别是半路转行的小伙伴或者没有系统学习过计算机基础的小伙伴. 对于想学而不知道怎么学的小伙伴,我可以分享下我的策略: 刷一 ...

  5. 【LeetCode】剑指 Offer 04. 二维数组中的查找

    二维数组查找:线性查找法 有二维数组: [  [1,   4,  7, 11, 15],  [2,   5,  8, 12, 19],  [3,   6,  9, 16, 22],  [10, 13, ...

  6. Deep Residual Learning for Image Recognition (ResNet)

    目录 主要内容 代码 He K, Zhang X, Ren S, et al. Deep Residual Learning for Image Recognition[C]. computer vi ...

  7. Java初学者作业——编写程序计算实发工资(实践1)

    返回本章节 返回作业目录 需求说明: 腾讯为Java工程师提供了基本工资(8000元).物价津贴及房租津贴.其中物价津贴为基本工资的40%,房屋津贴为基本工资的25%.要求编写程序计算实发工资. 实现 ...

  8. HTML网页设计基础笔记 • 【第6章 背景和阴影】

    全部章节   >>>> 本章目录 6.1 背景属性 6.1.1 背景颜色 6.1.2 背景图片 6.1.3  背景图片的重复方式 6.2 背景图片的定位 6.2.1 backg ...

  9. SpringCloud创建Eureka模块集群

    1.说明 本文详细介绍Spring Cloud创建Eureka模块集群的方法, 基于已经创建好的Spring Cloud Eureka Server模块, 请参考SpringCloud创建Eureka ...

  10. xxd命令转换二进制十六进制文件

    Linux下的xxd命令,可以把文件在二进制和十六进制之间互相转换. 1.准备需要转换的二进制文件 这个二进制文件可以是任意格式的, 示例中我们创建一个txt格式的二进制文件, vi demo.txt ...