Java Stream 实用特性:排序、分组和 teeing

2023-11-14

排序

基本数据类型排序

基本数据类型就是字符串、整型、浮点型这些,也就是要排序的列表中的元素都是这些基本类型的,比如 List<Integer>的。

下面就用一个整型列表举例说明。

正序排序

正序排序,也可以叫做按照自然顺序排序,对于整型来说就是从小到大的。

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
  integerList.add(i);
}
List<Integer> collect = integerList.stream()
  .sorted()
  .collect(Collectors.toList());
System.out.println(collect);
复制代码

输出结果是 [0, 1, 2, 3, 4],这很简单没什么好说的。

倒序排序

List<Integer> integerList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
  integerList.add(i);
}
List<Integer> collect2 = integerList.stream()
  .sorted(Comparator.reverseOrder())
  .collect(Collectors.toList());
System.out.println(collect2);
复制代码

倒序排就是从大到小排序,也很简单在 sorted()方法中添加 Comparator.reverseOrder() 就可以了。

非基本类型实体排序

基本类型的列表排序很简单,但是在实际项目中用到的情况不太多,经常用到的还是我们自定义类型的排序,比如项目中有一个用户实体、一个订单实体、一个产品实体等。

首先定一个Product实体类:

import lombok.Data;

/**
 * @author fengzheng
 */
@Data
public class Product {
    /**
     * 唯一标示
     */
    private Integer id;

    /**
     * 所属类别
      */
    private Integer type;

    /**
     * 商品名称
     */
    private String name;
    
    /**
     * 价格
      */
    private Double price;
    
}
复制代码

按某一个字段排序

对应到我上面定义的这个实体,可以是按照 id 排序,或者按照 price排序。

正序排序

假设按照 price从小到大排序,也就是按照价格由低到高排序。

对应到 SQL 上,可以表示成这样的。

select * from product order by price asc
复制代码

那用 Stream 实现呢?

List<Product> productList = initProductList();
List<Product> collect = productList.stream()
  .sorted(Comparator.comparing(Product::getPrice))
  .collect(Collectors.toList());
复制代码

等价于

List<Product> collect = productList.stream()
  .sorted((x,y) -> x.getPrice().compareTo(y.getPrice()))
  .collect(Collectors.toList());
复制代码

等价于

Comparator<Product> comparator = new Comparator<Product>() {
  @Override
  public int compare(Product p1, Product p2) {
    return p1.getPrice().compareTo(p2.getPrice());
  }
};

List<Product> collect = productList.stream()
  .sorted((p1, p2) -> comparator.compare(p1, p2))
  .collect(Collectors.toList());
复制代码

这里面主要由我们提供自定义的就是函数式接口 Comparator,凡是实现了 compare () 方法的都可以。

上面我们自定义的这个 comparator,重载了 compare方法。compare 方法的返回值规则:

  1. 前者小于后者,返回 -1;
  2. 前者大于后者,返回 1;
  3. 前者等于后者,返回 0;

所以可以理解为,如果 compare 返回的是 1, Stream 就会交换两个实体的位置。所以这样一来,倒序排序就很好整了。

倒序排序

可以这样写,使用 reversed() 方法

List<Product> collect = productList.stream()
  .sorted(Comparator.comparing(Product::getPrice).reversed())
  .collect(Collectors.toList());
复制代码

或者可以

List<Product> collect = productList.stream()
	.sorted(Comparator.comparing(Product::getPrice,Comparator.reverseOrder()))
  .collect(Collectors.toList());
复制代码

还可以直接直接使用compare ,倒序排序就简单了,稍微改一下就好了。

直接用 Lambda 表达式的写法

List<Product> collect = productList.stream()
  .sorted((x,y) -> y.getPrice().compareTo(x.getPrice()))
  .collect(Collectors.toList());
复制代码

等价于,抽取出自定义 Comparator的方法

Comparator<Product> comparator = new Comparator<Product>() {
            @Override
            public int compare(Product p1, Product p2) {
                return p2.getPrice().compareTo(p1.getPrice());
            }
};

List<Product> collect = productList.stream()
  .sorted((p1, p2) -> comparator.compare(p1, p2))
  .collect(Collectors.toList());
复制代码

倒序和正序的区别其实就是将 compare()前后两个元素的位置对调一下。

对于大小比较的可以直接用 compare()方法,但是有一些情况可能不止这么简单。没有关系,我们不是可以自定义 Comparator 吗,在 Comparator 重写的 compare 方法中可以加入我们的排序逻辑,不管多么特殊、多么复杂,只要返回一个 int 类型的就可以了。

按照多个字段排序

还有一些情况要按照两个甚至多个字段排序,一个主排序,一个次要排序。比如我们想要先按 type 升序,再按 price 降序。

对应到 SQL 上就像这样

select * from product order by type asc,price desc
复制代码

那用 Stream 来实现是怎么样的呢?用 thenComparing连接多个要排序的属性。

List<Product> collect = productList.stream().sorted(Comparator.comparing(Product::getType).thenComparing(Product::getPrice, Comparator.reverseOrder())).collect(Collectors.toList());
复制代码

或者还可以定义两个 Comparator

Comparator<Product> typeComparator = new Comparator<Product>() {
  @Override
  public int compare(Product p1, Product p2) {
    return p1.getType().compareTo(p2.getType());
  }
};

Comparator<Product> priceComparator = new Comparator<Product>() {
  @Override
  public int compare(Product p1, Product p2) {
    return p2.getPrice().compareTo(p1.getPrice());
  }
};
List<Product> collect = productList.stream()
  .sorted(typeComparator.thenComparing(priceComparator))
  .collect(Collectors.toList());
复制代码

怎么样,一点难度都没有吧。

分组

除了排序,还有一个非常有用而且经常会用的功能就是分组功能。分组功能是 collect()方法提供的功能,返回值是一个字典类型。

根据 type 进行分组

对应到 SQL 中就是下面这样

select * from product group by type
复制代码

用 Stream 来实现呢,就是下面这样子

Map<Integer, List<Product>> map = productList.stream()
  .collect(Collectors.groupingBy(Product::getType));
复制代码

最后生成的对象是一个 Map 类型,key 是用来作为分组依据的字段值,value 是一个列表,也就是同一组的对象集合。在这个例子中,key 就是 product 对象的 type 属性,value 就是 type 相同的 Product 对象的集合。

如果只是求出每一个组所包含的对象个数,可以这样实现,不用遍历 Map 这么麻烦。

Map<Integer, Long> map = productList.stream()
  .collect(Collectors.groupingBy(Product::getType, Collectors.counting()));
复制代码

根据两个或多个字段分组

有时候我们可能会根据不止一个字段进行分组,比如想按照类别相同且价格相同进行分组。

Map<String, List<Product>> map = productList.stream()
                .collect(Collectors.groupingBy(p -> p.getType() + "|" + p.getPrice()));
复制代码

等价于,将分组依据单独抽取出一个方法,这样就可以加入比较复杂的逻辑了,最终返回的是一个字符串。

Map<String, List<Product>> map = productList.stream()
   .collect(Collectors.groupingBy(p -> buildGroupKey(p)));

private static String buildGroupKey(Product p) {
   return p.getType() + "|" + p.getPrice();
}
复制代码

为什么两个字段之间要加一个分隔符呢,这是因为有些情况我们还会用到分组依据中的某一个字段,加入分隔符之后方便拆分字符串。当然了,也可以拿到这个分组下的任意一个元素获取。

嵌套分组

上面的根据多个字段分组是把多个字段当做同一级别并且的关系处理,还有一些时候呢,我们想要先按一个字段分组,再分组中再按另一个字段分组,这样就形成了一个嵌套关系,比如先按 type 分组,再按 price 分组,这就相当于是一个二维字典(两个层级)。

Map<Integer, Map<Double, List<Product>>> map = productList.stream()
  .collect(Collectors.groupingBy(Product::getType, Collectors.groupingBy(Product::getPrice)));
复制代码

通过返回值类型就可以看出来是怎么样的一个层级关系。

teeing()

这是 JDK 12 才出来的方法,所以要用这个方法,比如在 JDK12 以上才行。它的作用是对两个收集器(Collectors)的结果进行处理。上面的例子中,求出最高价格和最低价格的,并输出为一个字符串,将两个价格用 ~符号连接。

String result = productList.stream().collect(Collectors.teeing(
  Collectors.minBy(Comparator.comparing(Product::getPrice)),
  Collectors.maxBy(Comparator.comparing(Product::getPrice)),
  (min, max) -> {
    return min.get().getPrice() + "~" + max.get().getPrice();
  }
));
System.out.println(result);
复制代码

最终得到的结果是一个字符串,打印如下,测试数据没有做小数位限制。

4.347594572793579~89.43160979811124
复制代码

最终的返回类型根据teeing() 方法的最后一个参数的返回结果而定。 min 和 max 这两个参数就是前两个收集器 Collectors.minByCollectors.maxBy的返回结果,因为返回类型是 Optional ,所以再取值的时候要加上 get

总结

Stream 提供了很丰富的 API ,最大的好处是让我们可以少写很多代码,熟练掌握之后,可以在一些对应的场景快速实现我们想要的逻辑。

有同学说,不行啊,又是 filter 、又是 collect、又是 Collectors ,根本记不住啊。没关系,记不住也正常,它本来就是一个工具,我们其实只要知道它可以实现什么功能,具体的用法可以随用随查吗。这不,我的这两篇文章就可以放进收藏夹里,什么时候用,什么时候打开查一下就好了。

下次碰到类似的场景,记得用 Stream 试一下吧。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java Stream 实用特性:排序、分组和 teeing 的相关文章

  • 如何让 Spring 控制器从 POJO 返回 CSV? [复制]

    这个问题在这里已经有答案了 给定一个简单的 Java 对象 public class Pojo private String x private String y private String z getters setters 是否有一些
  • OpenJDK 11 问题 - 客户端在上次 UNWRAP 之前完成握手

    我正在将代码库从 Oracle Java 1 8 0 131 迁移到 OpenJDK 11 0 1 我们有实现 nio ssl 套接字通道的代码 在 Java 8 中 客户端 服务器握手工作正常 在 Java 11 中 客户端在解包来自服务
  • Eclipse 调试“未找到源”

    我刚刚开始使用 Eclipse 所以慢慢来吧 但是 当尝试调试 JUnit 测试用例时 我会收到一个对话框 指出当我在测试方法中的代码中找到此行时 未找到源代码 Assert assertEquals 1 contents size 我知道
  • 在这种情况下,我如何处理 Function 和省略号/可变参数?

    我的项目之一是抛出 lambda 表达式 https github com fge throwing lambdas 我的目标是简化潜在的使用 FunctionalInterfaces in Streams 其在流中使用的唯一 缺陷 是它们
  • 如何实现可运行队列

    我正在尝试实现一个可运行队列 在异步任务期间依次执行 意味着队列中的下一个将在另一个完成后执行 我编写了一个管理器来管理这些可运行对象和本身就是可运行对象的任务 然后 我获取异步任务中的第一个任务并运行它 希望它能够在队列中运行 但是它最终
  • 多个罐子、单个持久单元解决方案?

    包括我在内的一些人一直在努力将不同模块 jar 中的实体合并到单个持久性单元中 尤其是JavaSE 例如这里JPA 2 0 自动从不同的 jar 添加实体类到 PersistenceUnit https stackoverflow com
  • Android 上 WebRTC 的自定义视频源

    Overview 我想使用自定义视频源通过 WebRTC Android 实现来直播视频 如果我理解正确的话 现有的实现仅支持 Android 手机上的前置和后置摄像头 以下类与此场景相关 Camera1Enumerator java ht
  • 将 Flash 文件上传与 JSF 集成

    我看到我们可以通过flash文件上传来上传多个文件 喜欢SWF上传 http code google com p swfupload or YUI上传器 http yuilibrary com yui docs uploader 是否可以将
  • 动态更新 LookAndFeel 值

    我希望能够动态更新 Swing GUI 的 LookAndFeel 属性 在本例中 我有一个简单的 Swing Awt 游戏 运行最初为 Nimbus 的游戏LookAndFeel 在启动后的各个时刻 我只想更改 比方说 一个细节 应用程序
  • Play Framework 2.5.1 路由和依赖项注入(适用于 Java)

    我的 路线 文件中有这个 POST accounts controllers AccountsController createOneAccount 在我的 AccoutsController java 中 package controll
  • 处理大数据表时应该如何使用Hibernate Mapping

    问题定义 我有一个包含大量数据 超过 100 000 行 的数据库表 表结构如下 AppID DocID DocStatus 1 100 0 1 101 1 2 200 0 2 300 1 每个 applicationID 可能有数千个文档
  • 无法为对象堆保留足够的空间

    每次尝试运行该程序时 我都会重复出现以下异常 VM初始化期间发生错误 无法为对象堆保留足够的空间 无法创建Java虚拟机 我尝试增加虚拟内存 页面大小 和 RAM 大小 但无济于事 我怎样才能消除这个错误 运行 JVM XX MaxHeap
  • Windows:如何获取所有可见窗口的列表?

    无论如何都要使用相关技术重新标记 我不知道它们是什么 稍后我可能会提出更详细的问题 关于具体细节 但现在我正在尝试掌握 大局 我正在寻找一种方法来枚举 Windows 上的 真实可见窗口 我所说的 真正可见的窗口 就是指 用户所说的 窗口
  • 如何在 JUnit 中缩短(或隐藏)包名称?

    我在 JUnit 中有很长的包名称 这使得很难看到正在运行哪些测试 不幸的是 使用 Eclipse 的 缩写包名称 不起作用 有没有办法隐藏或者最好缩短它们 None
  • Java编译错误:包不存在

    在我的工作区 wsPrivate 中 我有 3 个 gradle 项目 刽子手 像素视图 Reports PixelView 和 Reports 项目编译良好 然而 Hangman 使用这两个项目 并且有些在编译时找不到包 请参阅以下错误
  • 告诉 JAXB 使用注释将 解组为 Date 类

    将 JAXB 与 Java First 一起使用时 类型的字段 属性java util Date编组和解编为xs dateTime一切都按预期进行 但是如果字段 属性的类型是Object JAXB 解组xs dateTimeto XMLGr
  • 部署到 Glassfish 4.1 时 URL 模式无效

    如果用户已经通过身份验证 我有一个网络过滤器可以从登录和索引页面重定向 最初我有一个无效的 URL 模式 我修复了无效模式并尝试重新部署以接收以下内容 java lang IllegalArgumentException Invalid U
  • DOM 中不再存在缓存元素

    就像在类似的问题中一样 我使用appium java 尝试选择元素 在移动应用程序中 我要转到页面 之后有许多元素 android widget ImageView 0 我需要选择 6 个 例如 这样的元素并执行其他步骤 Byt 只能选择一
  • startDrag 方法 已弃用且无法编译程序

    startDrag android content ClipData android view View DragShadowBuilder java lang Object int 已弃用 如何解决这个问题而又不失去对旧版本的兼容性 还有
  • 与 System.in.read() 一起使用的文件结尾/流键盘组合是什么

    如果这个小问题已经得到解答 我深表歉意 我无法在SO找到它 使用以下 Java 简单代码从 IDE 控制台读取行 Windows 7 和 Eclipse Kepler int v try while v System in read 1 S

随机推荐

  • MySQL 8.0 密码策略修改

    首次登录mysql 需要修改原始密码 mysql gt show databases ERROR 1820 HY000 You must reset your password using ALTER USER statement befo
  • Ground Rules

    Ground Rules 1 Ground rules ground ra nd n 地面 土 土地 土壤 v 触海底 使停飞 阻止 起飞 罚 儿童 不准出去玩 adj 磨细的 剁碎的 rule ru l n 规则 统治 控制 管理 v 统
  • C语言:getchar( ) 函数详解

    文章目录 一 getchar 函数定义 二 函数返回值 三 注意区分 getchar 和 scanf 四 getchar 的使用实例 一 getchar 函数定义 getchar 字符输入函数 没有参数 从输入缓冲区里面读取一个字符 一次只
  • 解决EasyExcel导出文件LocalDateTime报错问题

    文章目录 问题引出 解决方案 自定义Converter 引用 LocalDateTimeConverter 搞定收工 问题引出 狗子我在参与一个项目的时候接触到数据表格导出为Excel表格的需求 但是在导出的时候会出现报错 Cannot f
  • 代码走查和代码审查_代码审查是个好主意的其他原因

    代码走查和代码审查 什么是代码审查 What are Code Reviews A Code Review is essentially what it sounds like a review of code before it is a
  • 虚拟机由于电脑未正常关机打不开问题-两种情况

    虚拟机由于电脑未正常关机打不开问题 我来总结我遇到的两种情况 文章目录 虚拟机由于电脑未正常关机打不开问题 第一种情况 报错 锁定文件失败 第二种情况 报错 指定的虚拟磁盘需要进行修复 第一种情况 报错 锁定文件失败 如果出现锁定文件失败
  • Unity自动创建脚本及预制体并绑定

    自己写了一套流程控制的框架 根据不同的状态执行不同的命令 每个状态判断和命令都是一个场景中的物体 不想每次重复同样的操作 创建脚本再创建预制体再绑定脚本 所以尝试写了一个自动创建脚本与预制体的工具 StateMachineEditorUti
  • 指针解析 (*&p和&*p)

    p指向a p的值是a的地址 的作用 定义 int p 定义一个int类型的指针变量 取地址对应的数据 p 获取 下的值 p的值 对应的值 即 0x1000 5 p和 p的区别 p p 获取p的地址 即0x1008 p 即 0x1008 获取
  • matlab MinGW-w64 C/C++ Compiler 的配置(附百度云下载资源)

    环境 win10 matlab r2019b 起因 安装某matlab工具包时需要使用命令 mex setup 弹出常见错误 即需要编译器 两种编译器的尝试 由于那道墙的存在 让试错成本变得如此巨大 首先 matlab推荐了两种编译器 1
  • 漏洞公布平台汇总

    https www cnvd org cn https www seebug org https fr 0day today https www exploit db com https packetstormsecurity com
  • LU分解(matlab实现)

    LU分解 LU Decomposition 是矩阵分解的一种 可以将一个矩阵分解为一个下三角矩阵和一个上三角矩阵的乘积 主要的算法思路是从下至上地对矩阵A做初等行变换 将对角线左下方的元素变成零 这些行变换的效果等同于左乘一系列单位下三角矩
  • 区块链GAMEFI游戏——NFT+DeFi+游戏

    GameFi指的是将去中心化金融产品以游戏的方式呈现 将DeFi的规则游戏化 将游戏道具产品NFT化 即GameFi NFT DeFi 游戏 DeFi是GameFi的内核 NFT是去中心化的必备手段 而游戏是GameFi的外壳 NFT De
  • 视频编码格式发展史

    1 编码标准之战 想预测未来 就回顾历史 先来看看H 264这些编码的从标准化到现在普及的过程 人们一直在想尽办法提高视频编码的效率 让它在尽可能小的体积内提供最好的画面质量 从而满足人们对于视频传输 存储的需求 长期以来 视频编码标准主要
  • MySQL标准差和方差函数使用

    一 方差 方差是在概率论和统计方差衡量随机变量或一组数据时离散程度的度量 概率论中方差用来度量随机变量和其数学期望 即均值 之间的偏离程度 统计中的方差 样本方差 是每个样本值与全体样本值的平均数之差的平方值的平均数 在许多实际问题中 研究
  • matlab求二元函数极值算法_[小白头秃]多元函数基本概念总结

    1 基本概念 点集 区间 领域 一维 直线 实数集 线段 端点 不含端点 二维 平面 实平面 平面区域 边界 不含边界 三维 空间 实空间 曲面 边界 体 表面 不含边界 表面 点和点集的关系 内点 外点 边界点
  • 四叉树初步研究

    JS 四叉树初步研究 四叉树 为何要叫四叉树 二叉树与八叉树又是生么东东 看样子理解起来比较困难 实现该如何入手 树 就是树的结构 树根 树干 树枝 树叶 还有好吃的果实 o 其实树这种结构 很常见的 JSON 不就是最普通的树吗 四叉树的
  • html js清除缓存,js清除浏览器缓存的几种方法

    JS 缓存的问题一直都是我们又爱又恨的东西 也是我们比较头痛的问题 一方面为了提高网站响应速度 减少服务器的负担 和节省带宽 将需要将静态资源缓存在客户端 但是另一方面 当js 文件有改动的时候 如何快速的将客户端缓存的js文件都失效 这是
  • win环境下emacs实现markdown的html预览

    1 参考文档 https libraries io github jrblevin markdown mode 2 实现的核心思想是用pandoc生成html进行markdown的预览 把pandoc exe直接放到emacs bin目录下
  • windows 虚拟机相关功能、组件梳理

    简介 英文名称 中文名称 说明 Container 容器 Guarded Host 受保护的主机 利用远程证明创建并运行受防护的虚拟机 Hyper V Hyper V Management Tools Hyper V 管理工具 包含 GUI
  • Java Stream 实用特性:排序、分组和 teeing

    排序 基本数据类型排序 基本数据类型就是字符串 整型 浮点型这些 也就是要排序的列表中的元素都是这些基本类型的 比如 List