使用Stream处理集合数据【Java 1.8 新特性】

使用 Stream 处理集合数据【Java 1.8 新特性】

Stream 是Java 8中引入的一个重要概念,它提供了对集合对象进行一系列操作的新方式,包括筛选、转换、聚合等。Stream API以声明式方式提供了对数据集合的高效操作,并且可以并行处理数据。

首先构建一个类,下面举例用得着

@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class Customer {
    private String name; // 姓名
    private Integer age; // 年龄
    private Double score;// 积分
}

然后构建一个混乱的集合

ArrayList<Customer> customers = new ArrayList(){{
            add(new Customer("user01",10,7000.0));
            add(new Customer("user02",10,2000.0));
            add(new Customer("user03",20,3000.0));
            add(new Customer("user04",20,4000.0));
            add(new Customer("user05",30,5000.0));
            add(new Customer("user06",30,6000.0));
            add(new Customer("user07",40,1000.0));
        }};

中间操作

[!NOTE]

带有⭐ ⭐⭐ 的方法在平常开发中使用频率高

带有⭐ ⭐的方法在平常开发中使用频率中等

带有⭐ 的方法在平常开发中使用频率低

不带⭐ 的方法在平常开发中几乎不会用到

filter 过滤 ⭐⭐⭐

// 筛选出年龄大于20,积分大于1000 的用户并返回
List<Customer> collect = customers.stream()
  .filter(c -> c.getAge() > 20 && c.getScore() > 1000 )
  .collect(Collectors.toList());

对于 filter ,需掌握3个知识点

  1. filter 方法可以多次调用,比如先按年龄过滤,再按积分过滤

    List<Customer> collect = customers.stream()
      .filter(c -> c.getAge() > 20)
      .filter(c -> c.getScore() > 1000 )
      .collect(Collectors.toList());
    

    [!CAUTION]

    filter 的多次调用中,当前是在上一次过滤的基础上再过滤,举个例子:如果一条数据满足条件b但不满足条件a,而过滤顺序是条件a在前条件b在后,那么这条数据在条件a处就被过滤掉了,不会轮到条件b处理。

map 映射 ⭐ ⭐⭐

将流中的每个元素映射到另一个元素,这么说可能有点抽象,通俗来说就是操作集合中的对象。

// 将每个用户的积分+1
customers.stream().map(customer -> {
            customer.setScore(customer.getScore()+1);
            return customer;
        }).collect(Collectors.toList());

结果如下

0 = {Customer@706} "Customer(name=user01, age=10, score=7001.0)"
1 = {Customer@707} "Customer(name=user02, age=10, score=2001.0)"
2 = {Customer@708} "Customer(name=user03, age=20, score=3001.0)"
3 = {Customer@709} "Customer(name=user06, age=20, score=4001.0)"
4 = {Customer@710} "Customer(name=user06, age=30, score=5001.0)"
5 = {Customer@711} "Customer(name=user06, age=30, score=6001.0)"
6 = {Customer@712} "Customer(name=user06, age=40, score=1001.0)"

对于 map ,需掌握3个知识点

  1. map 方法可以多次调用,比如先处理年龄+1,再处理积分+1
  2. map 的lambda表达式有返回值,需要return,至于是否需要使用一个新集合作为变量接收结果,一般我不建议用,因为“新集合”是浅拷贝,修改了原数据,“新集合”中的数据也会跟着变
  3. 结尾不用.collect(Collectors.toList()),操作是无效的~

flatMap 将流中的每个元素替换为目标元素的流

flatMap 用于将流中的每个元素(这些元素通常是集合或数组)转换成流,然后将这些流连接起来形成一个单一的流。如下代码

// 如果customer的年龄大于20,就将当前的customer对象复制2份,否则复制3份
List<Customer> collect = customers.stream().flatMap(customer -> {
            return customer.getAge() > 20?
                    IntStream.range(0, 2).mapToObj(
                            i -> new Customer(customer.getName(), customer.getAge(), customer.getScore()))
                    : IntStream.range(0, 3).mapToObj(
                            i -> new Customer(customer.getName(), customer.getAge(), customer.getScore()));
        }).collect(Collectors.toList());

对于 flatMap ,需掌握3个知识点

  1. flatMap 方法可以多次调用
  2. lambda表达式返回的对象必须是一个Stream<U>类型
  3. 结尾不用.collect(Collectors.toList()),操作是无效的~

[!TIP]

平时开发几乎用不到 flatMap 方法,处理 List< List<Obj> > 型的数据用到过。

limit 截取 ⭐ ⭐

它用于截取流中的前 n 个元素,返回一个由原始流的前 n 个元素组成的新流。如果流中的元素少于 n 个,则返回包含所有元素的流

// 截取customers前两个元素
List<Customer> collect = customers.stream().limit(2).collect(Collectors.toList());

结果如下

0 = {Customer@707} "Customer(name=user01, age=10, score=7000.0)"
1 = {Customer@708} "Customer(name=user02, age=10, score=2000.0)"

对于 limit ,需掌握3个知识点

  1. limit 方法可以多次调用(一般不会连着调多次)
  2. limit 是浅拷贝,修改了原数据,“新集合”中的数据也会跟着变
  3. 结尾不用.collect(Collectors.toList()),操作是无效的~

sorted 排序 ⭐ ⭐⭐

/**
 * 顺序排序(两种方案均可)
 * */
List<Customer> collect = customers.stream()
                .sorted(Comparator.comparingInt(Customer::getAge))
                .collect(Collectors.toList());
/**
 * 倒序排序,加 reversed 即可
 * */
List<Customer> collect = customers.stream()
                .sorted(Comparator.comparingInt(Customer::getAge).reversed())
                .collect(Collectors.toList());

对于 sorted ,需掌握3个知识点

  1. sorted 是浅拷贝,修改了原数据,“新集合”中的数据也会跟着变

  2. 结尾不用.collect(Collectors.toList()),操作是无效的~

  3. sorted 方法可以多次调用,比如先按年龄排,再按积分排

    List<Customer> collect = customers.stream()
                    .sorted(Comparator.comparingInt(Customer::getAge))
                    .sorted(Comparator.comparingDouble(Customer::getScore).reversed())
                    .collect(Collectors.toList());
    

    [!CAUTION]

    但是哈,这种排序会扰乱上一步排序的结果,比如上述代码,其实我希望是在年龄排序的基础上,再排序积分,结果如下,可以看出年龄排序已经被打乱了~,所以一般不会用到多次调用 sorted

    0 = {Customer@734} "Customer(name=, age=10, score=7000.0)"
    1 = {Customer@739} "Customer( age=30, score=6000.0)"
    2 = {Customer@738} "Customer( age=30, score=5000.0)"
    3 = {Customer@737} "Customer( age=20, score=4000.0)"
    4 = {Customer@736} "Customer( age=20, score=3000.0)"
    5 = {Customer@735} "Customer( age=10, score=2000.0)"
    6 = {Customer@740} "Customer( age=40, score=1000.0)"

    既然如此,那么如何达到要求呢?当然是从Comparator 上下手了,在第一个排序条件后跟.thenComparing...,如下

    List<Customer> collect = customers.stream()
                 .sorted(
    										Comparator.comparingInt(Customer::getAge)
    										.thenComparingDouble(Customer::getScore)
    								).collect(Collectors.toList());
    

    排序结果如下,达到要求了~

    0 = {Customer@737} "Customer( age=10, score=2000.0)"
    1 = {Customer@738} "Customer( age=10, score=7000.0)"
    2 = {Customer@739} "Customer( age=20, score=3000.0)"
    3 = {Customer@740} "Customer( age=20, score=4000.0)"
    4 = {Customer@741} "Customer( age=30, score=5000.0)"
    5 = {Customer@742} "Customer( age=30, score=6000.0)"
    6 = {Customer@743} "Customer( age=40, score=1000.0)"

    [!NOTE]

    • thenComparing支持多种数据类型排序:int,long,double
    • Comparator 也支持链式编程

mapToDouble / mapToInt / mapToLong 提取数字 ⭐⭐

用于将流中的每个元素映射到一个 double 值。这个操作通常用于将流中的元素转换成数值型数据,以便进行数值计算。这些 方法接受一个函数作为参数,这个函数会被应用到流中的每个元素上,并将每个元素转换成一个 数字 值。这个方法返回一个 DoubleStream / LongStream / IntStream,可以被进一步用于数值操作,如求和、平均值计算等。

// 把customers中的各元素的积分值提取出来装入一个list
List<Double> collect = customers.stream().mapToDouble(Customer::getScore).boxed().collect(Collectors.toList());
// 计算积分总和
double totalScore = customers.stream().mapToDouble(Customer::getScore).sum();
// 获取最大积分
double maxScore = customers.stream().mapToDouble(Customer::getScore).max();
// 获取最小积分
double minScore = customers.stream().mapToDouble(Customer::getScore).min();
// 计算平均积分
double minScore = customers.stream().mapToDouble(Customer::getScore).average();

方法还有很多,感兴趣可以去探索,这里就不一一例举了~

对于 mapToDouble / mapToInt / mapToLong ,需掌握1个知识点

  1. mapToDouble / mapToInt / mapToLong 可以链式编程,但是一般开发不这么做

终止操作

forEach:遍历流中元素⭐⭐⭐

customers.stream().forEach(
                customer -> {
                  	Double score = customer.getScore();
                    customer.setScore(score + 1);
                }
        );

对于 forEach ,需掌握2个知识点

  1. forEach 中如果修改数据,影响原集合
  2. forEach 方法仅能一次调用

collect:将流转换成其他形式(如集合)⭐⭐⭐

需掌握 Collectors工具类,它提供了多种收集方式

  • Collectors.toList():将流收集到一个新的 List 中 ⭐⭐⭐
  • Collectors.toSet():将流收集到一个新的 Set 中,这会去除重复元素 ⭐⭐⭐
  • Collectors.toCollection():将流收集到给定的 Collection
  • Collectors.joining():将流中的元素连接成一个字符串 ⭐
  • Collectors.toMap():将流元素收集到一个 Map 中 ⭐⭐⭐
  • Collectors.groupingBy():根据某个属性对流元素进行分组,结果收集到一个 Map 中⭐
  • Collectors.reducing():通过某个连接动作(如加法)将所有元素汇总成一个汇总结果⭐
  • Collectors.collectingAndThen():在另一个收集器的结果上执行映射操作
  • 除了使用 Collectors 提供的静态方法,开发者还可以自定义收集器(我没这么玩过,感兴趣的可以研究一下)

对于 collect ,需掌握1个知识点

  1. collect 方法仅能一次调用

reduce:通过某个连接动作将所有元素汇总成一个汇总结果 ⭐⭐

它用于通过某个连接动作将所有元素汇总成一个汇总结果。reduce 方法可以用于多种类型的归约操作,比如求和、求最大值、求最小值等。【做大数据项目时用的最多】

// 计算customers中各元素积分总和
double reduce = customers.stream().mapToDouble(i -> i.getScore()).reduce(0, (a, b) -> a + b);
// 计算customers中积分最大值
double reduce = customers.stream().mapToDouble(i -> i.getScore()).reduce(0, (a, b) -> a > b ? a:b);
// 计算customers中积分最小值
double reduce = customers.stream().mapToDouble(i -> i.getScore()).reduce(0, (a, b) -> a < b ? a:b);

对于 reduce ,需掌握1个知识点

  1. reduce 方法仅能一次调用

allMatchanyMatchnoneMatch:检查流中的元素是否与给定的条件匹配

// 是否所有customers中元素的积分数值都大于10
boolean b = customers.stream().allMatch(i -> i.getScore() > 10);
// customers中是否存在有的元素的积分数值大于10
boolean b = customers.stream().anyMatch(i -> i.getScore() > 10);
// customers中所有的元素的积分数值都不大于10
boolean b = customers.stream().noneMatch(i -> i.getScore() > 10);

对于 allMatchanyMatchnoneMatch ,需掌握1个知识点

  1. allMatchanyMatchnoneMatch 方法仅能一次调用

count:返回流中元素的数量 ⭐

// 统计customers中年龄大于20的数据量
long count = customers.stream().filter( i -> i.getAge() > 20 ).count();

对于 count ,需掌握1个知识点

1. `count` 方法仅能一次调用

findFirst、findAny:返回流中的第一个或任意一个元素 ⭐

Customer any = customers.stream().findAny().get();
Customer first = customers.stream().findFirst().get();

对于 findFirstfindAny ,需掌握3个知识点

  1. findFirstfindAny 方法仅能一次调用
  2. Optional 类的 orElse 方法允许你提供一个默认值,如果 Optional 为空,则返回这个默认值
  3. orElseGet:类似于 orElse,但接受一个 Supplier 函数,当 Optional 为空时,会调用这个函数来生成默认值

拓展: parallelStream 的使用

parallelStream 是 Java 8 引入的 Stream API 的一部分,它允许开发者并行处理集合中的元素。当在集合上调用 parallelStream() 方法时,会得到一个并行流,这个流可以利用多个CPU核心加速处理过程。相较于 Stream 有如下优势

  • 方法和 Stream 相差无几,上述的方法也可以在parallelStream中使用,举俩例子如下

    List<Customer> integerStream = arrayList.parallelStream().filter( i -> i.getAge() > 10 ).collect(Collectors.toList());
    
    arrayList.parallelStream().filter( i -> i.getAge() > 10 ).forEach(i -> System.out.println(i));
    
  • 并行处理能力:parallelStream 利用 Java 的 ForkJoinPool 线程池来并行处理数据,这意味着它可以同时在多个 CPU 核心上执行任务,从而加速处理过程。

  • 性能提升:对于计算密集型任务,parallelStream 可以显著减少总体执行时间,因为它允许多个处理器核心同时工作。

  • 更好的资源利用:在多核处理器系统上,parallelStream 可以更有效地利用所有可用的处理器核心,而顺序流只能在单个线程上执行。

  • 优化的算法实现:某些算法在并行执行时更加高效,因为它们可以被分解成多个独立的子任务,每个任务可以在不同的处理器核心上并行执行。

  • 处理大数据集:对于大规模数据集,parallelStream 可以提供更快的处理速度,因为它避免了单个线程处理所有数据的瓶颈。

然而,parallelStream 也有一些局限性和需要注意的地方:

  • 线程安全:在 parallelStream 中执行的操作必须是线程安全的,因为多个线程可能会同时执行这些操作。

  • 顺序不确定性:由于并行执行,操作的顺序是不可预测的,这可能会影响到那些依赖于元素顺序的操作。

  • 额外的开销:并行流引入了额外的线程管理和任务调度开销,对于小数据集或简单的操作,这可能会导致性能下降。

  • 调试难度:并行流可能会使调试变得更加困难,因为多个线程的交互可能导致难以追踪的错误。

  • I/O 绑定操作:对于 I/O 绑定的操作(如文件读写、网络请求等),并行流可能不会带来性能提升,因为这些操作的速度受限于 I/O 速度,而不是 CPU 处理能力

可以这样理解:parallelStream 运用了多线程,在处理大量数据的场景下的速度比 Stream 明显高;可也就是因为多线程,一些顺序性操作是不可预测的。


先记录到这里,后续有总结会持续补上

posted @   勤匠  阅读(86)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示