Java 8 简化代码(2)

news2024/11/17 22:30:31

Stream 操作详解

为了方便你理解 Stream 的各种操作,以及后面的案例,我先把这节课涉及的 Stream 操作汇总到了一张图中。你可以先熟悉一下。

 在接下来的讲述中,我会围绕订单场景,给出如何使用 Stream 的各种 API 完成订单的统计、搜索、查询等功能,和你一起学习 Stream 流式操作的各种方法。你可以结合代码中的注释理解案例,也可以自己运行源码观察输出。

我们先定义一个订单类、一个订单商品类和一个顾客类,用作后续 Demo 代码的数据结构:

//订单类
@Data
public class Order {
    private Long id;
    private Long customerId;//顾客ID
    private String customerName;//顾客姓名
    private List<OrderItem> orderItemList;//订单商品明细
    private Double totalPrice;//总价格
    private LocalDateTime placedAt;//下单时间
}
//订单商品类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderItem {
    private Long productId;//商品ID
    private String productName;//商品名称
    private Double productPrice;//商品价格
    private Integer productQuantity;//商品数量
}
//顾客类
@Data
@AllArgsConstructor
public class Customer {
    private Long id;
    private String name;//顾客姓名
}

在这里,我们有一个 orders 字段保存了一些模拟数据,类型是 List。这里,我就不贴出生成模拟数据的代码了。

创建流

要使用流,就要先创建流。创建流一般有五种方式:

  • 通过 stream 方法把 List 或数组转换为流;
  • 通过 Stream.of 方法直接传入多个元素构成一个流;
  • 通过 Stream.iterate 方法使用迭代的方式构造一个无限流,然后使用 limit 限制流元素个数;
  • 通过 Stream.generate 方法从外部传入一个提供元素的 Supplier 来构造无限流,然后使用 limit 限制流元素个数;
  • 通过 IntStream 或 DoubleStream 构造基本类型的流。
//通过stream方法把List或数组转换为流
@Test
public void stream()
{
    Arrays.asList("a1", "a2", "a3").stream().forEach(System.out::println);
    Arrays.stream(new int[]{1, 2, 3}).forEach(System.out::println);
}

//通过Stream.of方法直接传入多个元素构成一个流
@Test
public void of()
{
    String[] arr = {"a", "b", "c"};
    Stream.of(arr).forEach(System.out::println);
    Stream.of("a", "b", "c").forEach(System.out::println);
    Stream.of(1, 2, "a").map(item -> item.getClass().getName()).forEach(System.out::println);
}

//通过Stream.iterate方法使用迭代的方式构造一个无限流,然后使用limit限制流元素个数
@Test
public void iterate()
{
    Stream.iterate(2, item -> item * 2).limit(10).forEach(System.out::println);
    Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.TEN)).limit(10).forEach(System.out::println);
}

//通过Stream.generate方法从外部传入一个提供元素的Supplier来构造无限流,然后使用limit限制流元素个数
@Test
public void generate()
{
    Stream.generate(() -> "test").limit(3).forEach(System.out::println);
    Stream.generate(Math::random).limit(10).forEach(System.out::println);
}

//通过IntStream或DoubleStream构造基本类型的流
@Test
public void primitive()
{
    //演示IntStream和DoubleStream
    IntStream.range(1, 3).forEach(System.out::println);
    IntStream.range(0, 3).mapToObj(i -> "x").forEach(System.out::println);

    IntStream.rangeClosed(1, 3).forEach(System.out::println);
    DoubleStream.of(1.1, 2.2, 3.3).forEach(System.out::println);

    //各种转换,后面注释代表了输出结果
    System.out.println(IntStream.of(1, 2).toArray().getClass()); //class [I
    System.out.println(Stream.of(1, 2).mapToInt(Integer::intValue).toArray().getClass()); //class [I
    System.out.println(IntStream.of(1, 2).boxed().toArray().getClass()); //class [Ljava.lang.Object;
    System.out.println(IntStream.of(1, 2).asDoubleStream().toArray().getClass()); //class [D
    System.out.println(IntStream.of(1, 2).asLongStream().toArray().getClass()); //class [J

    //注意基本类型流和装箱后的流的区别
    Arrays.asList("a", "b", "c").stream()   // Stream<String>
            .mapToInt(String::length)       // IntStream
            .asLongStream()                 // LongStream
            .mapToDouble(x -> x / 10.0)     // DoubleStream
            .boxed()                        // Stream<Double>
            .mapToLong(x -> 1L)             // LongStream
            .mapToObj(x -> "")              // Stream<String>
            .collect(Collectors.toList());
}

filter

filter 方法可以实现过滤操作,类似 SQL 中的 where。我们可以使用一行代码,通过 filter 方法实现查询所有订单中最近半年金额大于 40 的订单,通过连续叠加 filter 方法进行多次条件过滤:

//最近半年的金额大于40的订单
orders.stream()
        .filter(Objects::nonNull) //过滤null值
        .filter(order -> order.getPlacedAt().isAfter(LocalDateTime.now().minusMonths(6))) //最近半年的订单
        .filter(order -> order.getTotalPrice() > 40) //金额大于40的订单
        .forEach(System.out::println);  

如果不使用 Stream 的话,必然需要一个中间集合来收集过滤后的结果,而且所有的过滤条件会堆积在一起,代码冗长且不易读。

map

map 操作可以做转换(或者说投影),类似 SQL 中的 select。为了对比,我用两种方式统计订单中所有商品的数量,前一种是通过两次遍历实现,后一种是通过两次 mapToLong+sum 方法实现:

//计算所有订单商品数量
//通过两次遍历实现
LongAdder longAdder = new LongAdder();
orders.stream().forEach(order ->
        order.getOrderItemList().forEach(orderItem -> longAdder.add(orderItem.getProductQuantity())));

//使用两次mapToLong+sum方法实现
orders.stream().mapToLong(order ->
        order.getOrderItemList().stream()
        .mapToLong(OrderItem::getProductQuantity).sum()).sum());

显然,后一种方式无需中间变量 longAdder,更直观。

这里再补充一下,使用 for 循环生成数据,是我们平时常用的操作。现在,我们可以用一行代码使用 IntStream 配合 mapToObj 替代 for 循环来生成数据,比如生成 10 个 Product 元素构成 List:

//把IntStream通过转换Stream<Project>
IntStream.rangeClosed(1,10)
        .mapToObj(i->new Product((long)i, "product"+i, i*100.0))
        .collect(toList());

flatMap

接下来,我们看看 flatMap 展开或者叫扁平化操作,相当于 map+flat,通过 map 把每一个元素替换为一个流,然后展开这个流。

比如,我们要统计所有订单的总价格,可以有两种方式:

  • 直接通过原始商品列表的商品个数 * 商品单价统计的话,可以先把订单通过 flatMap 展开成商品清单,也就是把 Order 替换为 Stream,然后对每一个 OrderItem 用 mapToDouble 转换获得商品总价,最后进行一次 sum 求和;
  • 利用 flatMapToDouble 方法把列表中每一项展开替换为一个 DoubleStream,也就是直接把每一个订单转换为每一个商品的总价,然后求和。
//直接展开订单商品进行价格统计
System.out.println(orders.stream()
        .flatMap(order -> order.getOrderItemList().stream())
        .mapToDouble(item -> item.getProductQuantity() * item.getProductPrice()).sum());

//另一种方式flatMap+mapToDouble=flatMapToDouble
System.out.println(orders.stream()
        .flatMapToDouble(order ->
                order.getOrderItemList()
                        .stream().mapToDouble(item -> item.getProductQuantity() * item.getProductPrice()))
        .sum());

这两种方式可以得到相同的结果,并无本质区别。

sorted

sorted 操作可以用于行内排序的场景,类似 SQL 中的 order by。比如,要实现大于 50 元订单的按价格倒序取前 5,可以通过 Order::getTotalPrice 方法引用直接指定需要排序的依据字段,通过 reversed() 实现倒序:

//大于50的订单,按照订单价格倒序前5
orders.stream().filter(order -> order.getTotalPrice() > 50)
        .sorted(comparing(Order::getTotalPrice).reversed())
        .limit(5)
        .forEach(System.out::println);  

distinct

distinct 操作的作用是去重,类似 SQL 中的 distinct。比如下面的代码实现:

  • 查询去重后的下单用户。使用 map 从订单提取出购买用户,然后使用 distinct 去重。
  • 查询购买过的商品名。使用 flatMap+map 提取出订单中所有的商品名,然后使用 distinct 去重。
//去重的下单用户
System.out.println(orders.stream().map(order -> order.getCustomerName()).distinct().collect(joining(",")));

//所有购买过的商品
System.out.println(orders.stream()
        .flatMap(order -> order.getOrderItemList().stream())
        .map(OrderItem::getProductName)
        .distinct().collect(joining(",")));

skip & limit

skip 和 limit 操作用于分页,类似 MySQL 中的 limit。其中,skip 实现跳过一定的项,limit 用于限制项总数。比如下面的两段代码:

  • 按照下单时间排序,查询前 2 个订单的顾客姓名和下单时间;
  • 按照下单时间排序,查询第 3 和第 4 个订单的顾客姓名和下单时间。
//按照下单时间排序,查询前2个订单的顾客姓名和下单时间
orders.stream()
        .sorted(comparing(Order::getPlacedAt))
        .map(order -> order.getCustomerName() + "@" + order.getPlacedAt())
        .limit(2).forEach(System.out::println);
//按照下单时间排序,查询第3和第4个订单的顾客姓名和下单时间
orders.stream()
        .sorted(comparing(Order::getPlacedAt))
        .map(order -> order.getCustomerName() + "@" + order.getPlacedAt())
        .skip(2).limit(2).forEach(System.out::println);

collect

collect 是收集操作,对流进行终结(终止)操作,把流导出为我们需要的数据结构。

接下来,我通过 6 个案例,来演示下几种比较常用的 collect 操作:

第一个案例,实现了字符串拼接操作,生成一定位数的随机字符串。

第二个案例,通过 Collectors.toSet 静态方法收集为 Set 去重,得到去重后的下单用户,再通过 Collectors.joining 静态方法实现字符串拼接。

第三个案例,通过 Collectors.toCollection 静态方法获得指定类型的集合,比如把 List转换为 LinkedList。

第四个案例,通过 Collectors.toMap 静态方法将对象快速转换为 Map,Key 是订单 ID、Value 是下单用户名。

第五个案例,通过 Collectors.toMap 静态方法将对象转换为 Map。Key 是下单用户名,Value 是下单时间,一个用户可能多次下单,所以直接在这里进行了合并,只获取最近一次的下单时间。

第六个案例,使用 Collectors.summingInt 方法对商品数量求和,再使用 Collectors.averagingInt 方法对结果求平均值,以统计所有订单平均购买的商品数量。

//生成一定位数的随机字符串
System.out.println(random.ints(48, 122)
    .filter(i -> (i < 57 || i > 65) && (i < 90 || i > 97))
    .mapToObj(i -> (char) i)
    .limit(20)
    .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
    .toString());

//所有下单的用户,使用toSet去重后实现字符串拼接
System.out.println(orders.stream()
    .map(order -> order.getCustomerName()).collect(toSet())
    .stream().collect(joining(",", "[", "]")));

//用toCollection收集器指定集合类型
System.out.println(orders.stream().limit(2).collect(toCollection(LinkedList::new)).getClass());

//使用toMap获取订单ID+下单用户名的Map
orders.stream()
    .collect(toMap(Order::getId, Order::getCustomerName))
    .entrySet().forEach(System.out::println);

//使用toMap获取下单用户名+最近一次下单时间的Map
orders.stream()
    .collect(toMap(Order::getCustomerName, Order::getPlacedAt, (x, y) -> x.isAfter(y) ? x : y))
    .entrySet().forEach(System.out::println);

//订单平均购买的商品数量
System.out.println(orders.stream().collect(averagingInt(order ->
    order.getOrderItemList().stream()
    .collect(summingInt(OrderItem::getProductQuantity)))));

可以看到,这 6 个操作使用 Stream 方式一行代码就可以实现,但使用非 Stream 方式实现的话,都需要几行甚至十几行代码。

有关 Collectors 类的一些常用静态方法,我总结到了一张图中,整理一下思路:

groupBy

groupBy 是分组统计操作,类似 SQL 中的 group by 子句。

  • 第一个案例,按照用户名分组,使用 Collectors.counting 方法统计每个人的下单数量,再按照下单数量倒序输出。
  • 第二个案例,按照用户名分组,使用 Collectors.summingDouble 方法统计订单总金额,再按总金额倒序输出。
  • 第三个案例,按照用户名分组,使用两次 Collectors.summingInt 方法统计商品采购数量,再按总数量倒序输出。
  • 第四个案例,统计被采购最多的商品。先通过 flatMap 把订单转换为商品,然后把商品名作为 Key、Collectors.summingInt 作为 Value 分组统计采购数量,再按 Value 倒序获取第一个 Entry,最后查询 Key 就得到了售出最多的商品。
  • 第五个案例,同样统计采购最多的商品。相比第四个案例排序 Map 的方式,这次直接使用 Collectors.maxBy 收集器获得最大的 Entry。
  • 第六个案例,按照用户名分组,统计用户下的金额最高的订单。Key 是用户名,Value 是 Order,直接通过 Collectors.maxBy 方法拿到金额最高的订单,然后通过 collectingAndThen 实现 Optional.get 的内容提取,最后遍历 Key/Value 即可。
  • 第七个案例,根据下单年月分组统计订单 ID 列表。Key 是格式化成年月后的下单时间,Value 直接通过 Collectors.mapping 方法进行了转换,把订单列表转换为订单 ID 构成的 List。
  • 第八个案例,根据下单年月 + 用户名两次分组统计订单 ID 列表,相比上一个案例多了一次分组操作,第二次分组是按照用户名进行分组。
//按照用户名分组,统计下单数量
System.out.println(orders.stream().collect(groupingBy(Order::getCustomerName, counting()))
        .entrySet().stream().sorted(Map.Entry.<String, Long>comparingByValue().reversed()).collect(toList()));

//按照用户名分组,统计订单总金额
System.out.println(orders.stream().collect(groupingBy(Order::getCustomerName, summingDouble(Order::getTotalPrice)))
        .entrySet().stream().sorted(Map.Entry.<String, Double>comparingByValue().reversed()).collect(toList()));

//按照用户名分组,统计商品采购数量
System.out.println(orders.stream().collect(groupingBy(Order::getCustomerName,
        summingInt(order -> order.getOrderItemList().stream()
                .collect(summingInt(OrderItem::getProductQuantity)))))
        .entrySet().stream().sorted(Map.Entry.<String, Integer>comparingByValue().reversed()).collect(toList()));

//统计最受欢迎的商品,倒序后取第一个
orders.stream()
        .flatMap(order -> order.getOrderItemList().stream())
        .collect(groupingBy(OrderItem::getProductName, summingInt(OrderItem::getProductQuantity)))
        .entrySet().stream()
        .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
        .map(Map.Entry::getKey)
        .findFirst()
        .ifPresent(System.out::println);

//统计最受欢迎的商品的另一种方式,直接利用maxBy
orders.stream()
        .flatMap(order -> order.getOrderItemList().stream())
        .collect(groupingBy(OrderItem::getProductName, summingInt(OrderItem::getProductQuantity)))
        .entrySet().stream()
        .collect(maxBy(Map.Entry.comparingByValue()))
        .map(Map.Entry::getKey)
        .ifPresent(System.out::println);

//按照用户名分组,选用户下的总金额最大的订单
orders.stream().collect(groupingBy(Order::getCustomerName, collectingAndThen(maxBy(comparingDouble(Order::getTotalPrice)), Optional::get)))
        .forEach((k, v) -> System.out.println(k + "#" + v.getTotalPrice() + "@" + v.getPlacedAt()));

//根据下单年月分组,统计订单ID列表
System.out.println(orders.stream().collect
        (groupingBy(order -> order.getPlacedAt().format(DateTimeFormatter.ofPattern("yyyyMM")),
                mapping(order -> order.getId(), toList()))));

//根据下单年月+用户名两次分组,统计订单ID列表
System.out.println(orders.stream().collect
        (groupingBy(order -> order.getPlacedAt().format(DateTimeFormatter.ofPattern("yyyyMM")),
                groupingBy(order -> order.getCustomerName(),
                        mapping(order -> order.getId(), toList())))));

如果不借助 Stream 转换为普通的 Java 代码,实现这些复杂的操作可能需要几十行代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1396621.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虚拟线程探索与实践

优质博文&#xff1a;IT-BLOG-CN 一、简介 虚拟线程是轻量级线程&#xff0c;极大地减少了编写、维护和观察高吞吐量并发应用的工作量。虚拟线程是由JEP 425提出的预览功能&#xff0c;并在JDK 19中发布&#xff0c;JDK 21中最终确定虚拟线程&#xff0c;以下是根据开发者反馈…

竞赛保研 大数据疫情分析及可视化系统

文章目录 0 前言2 开发简介3 数据集4 实现技术4.1 系统架构4.2 开发环境4.3 疫情地图4.3.1 填充图(Choropleth maps)4.3.2 气泡图 4.4 全国疫情实时追踪4.6 其他页面 5 关键代码最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 大数据疫…

[陇剑杯 2021]jwt

[陇剑杯 2021]jwt 题目做法及思路解析&#xff08;个人分享&#xff09; 问一&#xff1a;昨天&#xff0c;单位流量系统捕获了黑客攻击流量&#xff0c;请您分析流量后进行回答&#xff1a; 该网站使用了______认证方式。&#xff08;如有字母请全部使用小写&#xff09…

redis数据安全(二)数据持久化 RDB

目录 一、RDB快照持久化 原理 二、RDB快照持久化配置&#xff08;redis.conf&#xff09;&#xff1a; 三、触发RDB备份&#xff1a; 1、自动备份&#xff0c;需配置备份规则&#xff1a; 2、手动执行命令备份&#xff08;save | bgsave&#xff09;&#xff1a; 3、flus…

分销商城新零售商城门店商城小程序开发

用户注册&#xff1a;让用户用手机号或三方登录的方式轻松开启账号之旅。 商品探索&#xff1a;用户可以自由浏览琳琅满目的商品&#xff0c;还能通过关键词迅速锁定心仪之物。 商品分类与筛选&#xff1a;商品按类陈列&#xff0c;用户可根据价格、品牌等条件筛选&#xff…

【JS逆向学习】36kr登陆逆向案例(webpack)

在开始讲解实际案例之前&#xff0c;大家先了解下webpack的相关知识 WebPack打包 webpack是一个基于模块化的打包&#xff08;构建&#xff09;工具, 它把一切都视作模块 webpack数组形式&#xff0c;通过下标取值 !function(e) {var t {};// 加载器 所有的模块都是从这个…

《文科爱好者》是什么级别的期刊?是正规期刊吗?能评职称吗?

《文科爱好者》发展至今已有近四十年的历史。近四十年里无论最初由成都教育学院主办还是现在由成都大学主办&#xff0c;《爱好者》系列期刊一直坚持贴近学科教学实践&#xff0c;站在教育改革前沿&#xff0c;进行课程资源的深度开发&#xff0c;为基础教育课程改革提供全方位…

R 语言学习 case1:基础图形

step1: 安装缺失的库 install.packages("ggfun") install.packages("gcookbook")step2&#xff1a;导入库 library(ggplot2) library(ggfun) library(gcookbook)step3&#xff1a;使用数据 heightweight <- gcookbook::heightweight head(heightweig…

【好文翻译】JavaScript 中的 realm 是什么?

本文由体验技术团队黄琦同学翻译。 原文链接&#xff1a; https://weizmangal.com/2022/10/28/what-is-a-realm-in-js/ github仓库地址&#xff1a; https://github.com/weizman/weizman.github.io/blob/gh-pages/_posts/2020-02-02-what-is-a-realm-in-js.md 前言 作为我对…

NFT Insider #117:The Sandbox 与韩剧《还魂》合作推出人物化身

引言&#xff1a;NFT Insider由NFT收藏组织WHALE Members(https://twitter.com/WHALEMembers)、BeepCrypto&#xff08;https://twitter.com/beep_crypto&#xff09;联合出品&#xff0c;浓缩每周NFT新闻&#xff0c;为大家带来关于NFT最全面、最新鲜、最有价值的讯息。每期周…

外贸建站服务器如何选?海洋建站主机推荐?

外贸建站用哪个服务器比较好&#xff1f;独立网站怎么选择主机&#xff1f; 随着全球化的趋势&#xff0c;外贸网站的建设越来越受到企业的重视。然而&#xff0c;要想让外贸网站稳定、安全、可靠地运行&#xff0c;选择合适的外贸建站服务器是关键。海洋建站将详细介绍如何选…

学习笔记-李沐动手学深度学习(一)(01-07)

个人随笔 第三列是 jupyter记事本 官方github上啥都有&#xff08;代码、jupyter记事本、胶片&#xff09; https://github.com/d2l-ai 多体会 【梯度指向的是值变化最大的方向】 符号 维度 &#xff08;弹幕说&#xff09;2&#xff0c;3&#xff0c;4越后面维度越低 4…

C语言——atoi函数解析

目录 前言 atoi函数的介绍 atoi函数的使用 atoi函数的模拟实现 前言 对于atoi函数大家可能会有些陌生&#xff0c;不过当你选择并阅读到这里时&#xff0c;请往下阅读&#xff0c;我相信你能对atoi函数熟悉该函数的头文件为<stdlib.h> 或 <cstdlib> atoi函数的…

【分布式技术】监控平台zabbix对接grafana,优化dashboard

目录 第一步&#xff1a;在zabbix server服务端安装grafana&#xff0c;并启动 第二步&#xff1a; 访问http://ip:3000/login 第三步&#xff1a;创建数据源 第四步&#xff1a;导入dashboard模板 ps&#xff1a;自定义创建新面板 第一步&#xff1a;在zabbix server服务…

C语言入门第一节-初识C语言

C语言入门第一节-初识C语言 视频教程&#xff1a;C语言教程&#xff08;全网最具有比喻形象的&#xff09;&#xff1a;持续更新ing_哔哩哔哩_bilibili 一.C语言的介绍 由C编写应用&#xff1a;Unix , Linux, MySQL都是由C編写C程序由各种令牌组成&#xff0c;令牌可以是关键宇…

P1059 [NOIP2006 普及组] 明明的随机数————C++

目录 [NOIP2006 普及组] 明明的随机数题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示 解题思路Code运行结果 [NOIP2006 普及组] 明明的随机数 题目描述 明明想在学校中请一些同学一起做一项问卷调查&#xff0c;为了实验的客观性&#xff0c;他先用计算机生成了…

LabVIEW与微信开发数字液压缸测控系统

针对传统煤矿液压支架控制存在的精度和直线度问题&#xff0c;设计了一种数字液压缸测控系统&#xff0c;其核心是LabVIEW软件与微信小程序的结合&#xff0c;以及对应的精准硬件配置。该系统使用了NI CDAQ 9189数据采集控制器、脉冲输出模块和多种传感器&#xff08;MIK-P300压…

怎么提取伴奏?推荐4个好用软件

怎么提取伴奏&#xff1f;随着音乐在日常生活中的应用越来越广泛&#xff0c;人们对音乐的需求也日益增加。其中&#xff0c;伴奏作为音乐的重要组成部分&#xff0c;往往在创作、娱乐等方面起到关键作用。那么&#xff0c;如何从各种音乐资源中提取出伴奏呢&#xff1f;推荐使…

PyTorch 中的距离函数深度解析:掌握向量间的距离和相似度计算

目录 Pytorch中Distance functions详解 pairwise_distance 用途 用法 参数 数学理论公式 示例代码 cosine_similarity 用途 用法 参数 数学理论 示例代码 输出结果 pdist 用途 用法 参数 数学理论 示例代码 总结 Pytorch中Distance functions详解 pair…

N - Rightmost Digit

题目 Given a positive integer N, you should output the most right digit of N^N. 给定一个正整数 N&#xff0c;您应该输出 N^N 的最右边的数字。 Input The input contains several test cases. The first line of the input is a single integer T which is the number o…