0

0

Java Stream API:重构代码以避免共享可变性

聖光之護

聖光之護

发布时间:2025-10-01 11:04:17

|

854人浏览过

|

来源于php中文网

原创

Java Stream API:重构代码以避免共享可变性

本文探讨了在Java中处理数据库查询时,如何通过重构代码避免共享可变性问题。针对数据库参数限制导致的列表分批处理场景,我们分析了传统forEach循环中修改外部集合的弊端,并详细介绍了如何利用Java Stream API的map、flatMap和collect操作,以声明式、无副作用的方式高效地聚合数据,从而提升代码的纯净性、可读性和并发安全性。

1. 问题背景与共享可变性分析

在处理大数据量查询时,数据库通常会对sql语句中的参数数量有所限制。例如,一个in子句可能只接受最多500个参数。这意味着当我们需要根据一个包含数千个键的列表从数据库中获取数据时,必须将这个大列表分割成多个小批次进行查询。

原始代码示例展示了这种分批查询的常见实现方式:

AtomicInteger counter = new AtomicInteger();
List catList = new ArrayList<>(); // 共享的可变集合
List dogList = new ArrayList<>(); // 共享的可变集合

List numbers = Stream.iterate(1, e -> e + 1)
    .limit(5000)
    .collect(Collectors.toList());

// 将大列表分割成多个大小为500的子列表
Collection> partitionedListOfNumbers = numbers.stream()
    .collect(Collectors.groupingBy(num -> counter.getAndIncrement() / 500))
    .values();

// 遍历分批列表,并修改外部集合
partitionedListOfNumbers.stream()
    .forEach(list -> {
        List interimCatList = catRepo.fetchCats(list); // 从数据库获取Cat
        catList.addAll(interimCatList); // 修改外部catList
        List interimDogList = dogRepo.fetchDogs(list); // 从数据库获取Dog
        dogList.addAll(interimDogList); // 修改外部dogList
    });

上述代码的核心问题在于其使用了共享可变性(Shared Mutability)。catList和dogList是在forEach循环外部声明的,并在循环内部通过addAll方法不断被修改。这种模式在函数式编程中被视为“不纯”的操作,因为它产生了副作用(Side Effect)。共享可变性带来的弊端包括:

  • 难以理解和维护: 程序的行为依赖于执行顺序和外部状态,增加了心智负担。
  • 并发安全问题: 在多线程环境下,多个线程同时修改同一个共享集合可能导致数据不一致或竞态条件,需要额外的同步机制(如Collections.synchronizedList或CopyOnWriteArrayList),这会增加复杂性和开销。
  • 测试困难: 带有副作用的代码更难进行单元测试,因为测试结果可能受外部状态影响。

2. 解决方案:利用Java Stream API实现无副作用聚合

Java 8引入的Stream API提供了一种声明式、函数式的数据处理方式,可以有效地避免共享可变性。通过结合map、flatMap和collect操作,我们可以在不修改任何外部状态的情况下,将分批查询的结果聚合到新的集合中。

// 更简洁的数字列表生成方式
Collection> partitionedListOfNumbers = IntStream.rangeClosed(1, 5000)
    .boxed() // 将IntStream转换为Stream
    .collect(Collectors.groupingBy(num -> (num - 1) / 500)) // 修正分组逻辑,确保从0开始计数
    .values();

// 获取Cat列表:使用Stream API进行无副作用聚合
List catList = partitionedListOfNumbers.stream()
    .map(list -> catRepo.fetchCats(list)) // 将每个小批次键列表映射为List
    .flatMap(List::stream) // 将Stream>扁平化为Stream
    .collect(Collectors.toList()); // 收集所有Cat对象到新的List

// 获取Dog列表:同样使用Stream API进行无副作用聚合
List dogList = partitionedListOfNumbers.stream()
    .map(list -> dogRepo.fetchDogs(list)) // 将每个小批次键列表映射为List
    .flatMap(List::stream) // 将Stream>扁平化为Stream
    .collect(Collectors.toList()); // 收集所有Dog对象到新的List

代码解析:

立即学习Java免费学习笔记(深入)”;

  1. IntStream.rangeClosed(1, 5000).boxed(): 这是一个更简洁高效地生成Integer列表的方式,避免了AtomicInteger的引入,因为groupingBy的classifier函数是无状态的。boxed()将IntStream转换为Stream
  2. collect(Collectors.groupingBy(num -> (num - 1) / 500)).values(): 这一步负责将原始的数字列表按每500个一组进行分区。(num - 1) / 500作为分组键,确保每500个数字分到一个组中。
  3. partitionedListOfNumbers.stream(): 创建一个包含所有分批键列表的流。
  4. .map(list -> catRepo.fetchCats(list)): 这是核心转换步骤。对于流中的每个List(即一个批次的键),调用catRepo.fetchCats(list)方法从数据库中获取一个List。此时,流的类型是Stream>。
  5. .flatMap(List::stream): flatMap操作用于将一个流中的每个元素(这里是List)转换成一个流,然后将所有这些子流连接成一个单一的流。List::stream是一个方法引用,它将每个List转换成一个Stream。最终,我们得到了一个Stream
  6. .collect(Collectors.toList()): 最后,使用Collectors.toList()将扁平化后的所有Cat对象收集到一个新的List中。这个新列表是不可变的,因为它是在收集操作结束时一次性构建的,没有在过程中被外部修改。

通过这种方式,catList和dogList都是通过流操作的最终结果创建的新集合,避免了在处理过程中对外部共享状态的修改。

MusicLM
MusicLM

谷歌平台的AI作曲工具,用文字生成音乐

下载

3. 进一步抽象与代码复用

由于获取Cat列表和Dog列表的逻辑结构非常相似,我们可以进一步抽象,减少代码重复。这可以通过创建一个通用的辅助方法来实现,该方法接受一个函数作为参数,用于指定如何从数据库获取特定类型的实体。

import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

// 假设Cat和Dog类以及catRepo, dogRepo已定义

public class AnimalService {

    // 辅助方法,用于通用地从分批的键列表中获取实体
    private static  List fetchEntities(
        Collection> partitionedKeys,
        Function, List> fetchFunction) {

        return partitionedKeys.stream()
            .map(fetchFunction) // 应用传入的获取函数
            .flatMap(List::stream)
            .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        // 模拟数据仓库
        CatRepository catRepo = new CatRepository();
        DogRepository dogRepo = new DogRepository();

        // 生成并分区键列表
        Collection> partitionedListOfNumbers = IntStream.rangeClosed(1, 5000)
            .boxed()
            .collect(Collectors.groupingBy(num -> (num - 1) / 500))
            .values();

        // 使用辅助方法获取Cat列表
        List catList = fetchEntities(partitionedListOfNumbers, catRepo::fetchCats);
        System.out.println("Fetched " + catList.size() + " cats.");

        // 使用辅助方法获取Dog列表
        List dogList = fetchEntities(partitionedListOfNumbers, dogRepo::fetchDogs);
        System.out.println("Fetched " + dogList.size() + " dogs.");
    }
}

// 模拟实体类和仓库接口
class Cat {}
class Dog {}

class CatRepository {
    public List fetchCats(List keys) {
        // 模拟数据库查询
        return keys.stream().map(k -> new Cat()).collect(Collectors.toList());
    }
}

class DogRepository {
    public List fetchDogs(List keys) {
        // 模拟数据库查询
        return keys.stream().map(k -> new Dog()).collect(Collectors.toList());
    }
}

通过引入fetchEntities辅助方法,我们不仅避免了共享可变性,还提高了代码的模块化和复用性。Function, List>作为参数,使得该方法可以灵活地应用于任何接受List并返回List的数据库查询操作。

4. 总结与注意事项

通过Java Stream API的map、flatMap和collect操作,我们能够以声明式、函数式的方式重构代码,彻底避免了共享可变性问题。这种方法带来了多方面的好处:

  • 纯净性与可预测性: 函数不再有副作用,其输出仅依赖于输入,提高了代码的可预测性。
  • 并发安全性: 由于没有共享状态被修改,代码天然地更适合并发执行,尤其是在使用parallelStream()时。
  • 可读性与维护性: 声明式代码通常更简洁,更易于理解其意图,而非关注具体的执行步骤。
  • 易于测试: 无副作用的函数更容易进行单元测试,因为它们不依赖或修改外部状态。

注意事项:

  • 性能考量: 尽管Stream API提供了强大的抽象,但在某些极端性能敏感的场景下,手动循环可能略快。然而,对于大多数业务应用而言,Stream API带来的代码质量提升远超微小的性能差异。
  • 流的惰性求值: Stream是惰性求值的,只有当遇到终端操作(如collect)时,中间操作才会执行。
  • 理解map与flatMap的区别 map将每个元素转换成一个新的元素(可能是一个集合),而flatMap则将每个元素转换成一个流,并将所有这些流扁平化为一个单一的流。这是聚合分批结果的关键。

通过采纳这些函数式编程原则和Stream API的最佳实践,开发者可以编写出更健壮、更易于维护和扩展的Java应用程序。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
数据分析工具有哪些
数据分析工具有哪些

数据分析工具有Excel、SQL、Python、R、Tableau、Power BI、SAS、SPSS和MATLAB等。详细介绍:1、Excel,具有强大的计算和数据处理功能;2、SQL,可以进行数据查询、过滤、排序、聚合等操作;3、Python,拥有丰富的数据分析库;4、R,拥有丰富的统计分析库和图形库;5、Tableau,提供了直观易用的用户界面等等。

728

2023.10.12

SQL中distinct的用法
SQL中distinct的用法

SQL中distinct的语法是“SELECT DISTINCT column1, column2,...,FROM table_name;”。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

328

2023.10.27

SQL中months_between使用方法
SQL中months_between使用方法

在SQL中,MONTHS_BETWEEN 是一个常见的函数,用于计算两个日期之间的月份差。想了解更多SQL的相关内容,可以阅读本专题下面的文章。

350

2024.02.23

SQL出现5120错误解决方法
SQL出现5120错误解决方法

SQL Server错误5120是由于没有足够的权限来访问或操作指定的数据库或文件引起的。想了解更多sql错误的相关内容,可以阅读本专题下面的文章。

1263

2024.03.06

sql procedure语法错误解决方法
sql procedure语法错误解决方法

sql procedure语法错误解决办法:1、仔细检查错误消息;2、检查语法规则;3、检查括号和引号;4、检查变量和参数;5、检查关键字和函数;6、逐步调试;7、参考文档和示例。想了解更多语法错误的相关内容,可以阅读本专题下面的文章。

360

2024.03.06

oracle数据库运行sql方法
oracle数据库运行sql方法

运行sql步骤包括:打开sql plus工具并连接到数据库。在提示符下输入sql语句。按enter键运行该语句。查看结果,错误消息或退出sql plus。想了解更多oracle数据库的相关内容,可以阅读本专题下面的文章。

841

2024.04.07

sql中where的含义
sql中where的含义

sql中where子句用于从表中过滤数据,它基于指定条件选择特定的行。想了解更多where的相关内容,可以阅读本专题下面的文章。

581

2024.04.29

sql中删除表的语句是什么
sql中删除表的语句是什么

sql中用于删除表的语句是drop table。语法为drop table table_name;该语句将永久删除指定表的表和数据。想了解更多sql的相关内容,可以阅读本专题下面的文章。

423

2024.04.29

java入门学习合集
java入门学习合集

本专题整合了java入门学习指南、初学者项目实战、入门到精通等等内容,阅读专题下面的文章了解更多详细学习方法。

1

2026.01.29

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Kotlin 教程
Kotlin 教程

共23课时 | 3万人学习

C# 教程
C# 教程

共94课时 | 7.9万人学习

Java 教程
Java 教程

共578课时 | 53.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号