为什么用了 Stream,代码反而越写越丑了?

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:掘金/C站/腾讯云/阿里云/华为云/51CTO(全网同号);欢迎大家常来逛逛,互相学习。

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言

  众所周知,Java 8 新特性引入 Stream API, 它对集合框架是一个重要的扩展; 而且 Stream API 为我们提供了更简洁、更声明式的方式来处理数据集合。在很多情况下,Stream 可以使代码更简洁、可读性更高。然而,许多开发者发现,尽管 Stream 能够减少代码行数和实现逻辑的复杂度,但在实际开发中,使用 Stream 反而会导致代码变得更加难懂、可读性降低。为什么会出现这种情况呢?

  本文将带大家一起深入剖析使用 Stream 时可能带来的问题,并探讨如何避免这些问题。

1. Stream 的本意和优点

首先,我们来回顾一下 Stream 的优点:

  • 声明式编程 :通过 Stream,我们可以更像 SQL 查询一样表达业务逻辑,这种高层次的抽象使得代码变得更加简洁。
  • 并行处理Stream 提供了强大的并行处理支持,可以轻松地处理大规模数据。
  • 懒加载Stream 支持懒加载(Lazy Evaluation),意味着在没有最终操作(如 collect())时,数据并不会被处理,提高了性能。

示例:使用 Stream 计算偶数之和

java 复制代码
import java.util.Arrays;
import java.util.List;

/**
 * @Author 喵手
 * @date: 2025-04-14
 */
public class StreamTest {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
        int sum = numbers.stream()
                .filter(n -> n % 2 == 0)
                .mapToInt(Integer::intValue)
                .sum();
        System.out.println(sum); // 输出 12
    }
}

  在这个例子中,Stream 用简洁的声明式方式表达了"找出所有偶数并求和"的业务逻辑,代码简洁明了。

  就光看着如下代码,是不是非常简洁,使用起来也及其方便,比传统写法优雅多了。

2. 使用 Stream 时代码反而变丑的原因

  尽管 Stream 提供了许多优点,但有时在复杂的业务场景下,过度使用 Stream 或不当使用会导致代码变得更加难懂。以下是一些可能导致代码"越写越丑"的原因:

2.1. 过度链式调用

  Stream 的一个常见用法是链式调用,即将多个操作(如 filter()map()reduce())连接在一起。尽管这种链式调用在简单的场景中非常有效,但在复杂的业务逻辑中,过多的链式调用会使代码变得冗长且难以理解,尤其当每个操作的行为都不明确时。

示例:过度链式调用

java 复制代码
List<String> result = list.stream()
                           .filter(s -> s.length() > 3)
                           .map(s -> s.toUpperCase())
                           .filter(s -> s.startsWith("A"))
                           .distinct()
                           .collect(Collectors.toList());

  在这个例子中,多个操作被串联在一起,每个操作看起来都非常简洁,但是当业务逻辑变得复杂时,这种链式调用会让代码看起来像是"黑箱",很难理解每个操作的具体含义,有木有,特别是运维前同事的代码,就有种想拍屎它的感jio,写的都是啥!!!

问题:

  • 可读性差:链式调用让代码缺乏明显的步骤分隔,使得每个操作之间的关系不清晰。
  • 调试困难:如果某个中间操作出错,调试时很难快速定位问题。

2.2. 不当的懒加载与终止操作

  Stream 的懒加载特性虽然提高了性能,但在某些情况下,这种特性会导致代码执行的顺序不明确,容易让开发者迷失在操作的流程中。如果流式操作没有合适的终止操作(如 collect()forEach()),代码可能看起来没有执行任何实际操作。

示例:无终止操作

java 复制代码
List<String> strings = Arrays.asList("apple", "banana", "cherry");
strings.stream()
       .filter(s -> s.length() > 5)
       .map(s -> s.toUpperCase());

  在上面的代码中,Stream 中的操作不会执行,因为没有终止操作(如 collect()forEach())。这种懒加载特性会让人产生疑问:是不是代码缺少某些执行步骤?

问题:

  • 不明确的执行时机:流操作没有执行时,程序员很难理解这些操作是否实际被执行。
  • 缺少副作用 :如果没有适当的副作用(例如通过 forEach() 进行输出),代码的可操作性变差。

2.3. 调试和错误追踪困难

  由于 Stream 操作通常是基于函数式编程的,而函数式编程是高度抽象化的,调试过程可能变得困难。尤其是当 Stream 操作链较长时,很难逐步追踪执行过程并查看每个步骤的中间结果。

示例:调试困难

java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
                 .map(n -> n * 2)
                 .filter(n -> n > 5)
                 .reduce(0, Integer::sum);

  在上面的例子中,虽然代码简洁,但调试时可能难以迅速看到每个中间操作的结果。你可能会希望查看 map 后的数据,或者调试 filter 的逻辑,但这种函数式编程风格不太容易追踪。

问题:

  • 缺乏中间状态:无法像传统的 for 循环那样在每个步骤中查看数据。
  • 调试工具支持差:调试时不能轻松观察每个中间步骤的输出。

2.4. 性能问题

  尽管 Stream 提供了并行流处理的能力,但并行流在某些情况下可能引入性能问题。对于小规模的数据集,使用并行流反而会增加不必要的开销。过度依赖并行流可能会导致性能下降。

示例:并行流性能问题

java 复制代码
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.parallelStream()
                 .filter(n -> n > 2)
                 .mapToInt(Integer::intValue)
                 .sum();

  在这种情况下,数据集非常小,使用并行流处理反而增加了线程切换的开销,不会带来性能提升。

问题:

  • 不合适的并行化:过早地使用并行流,尤其是在数据量小或者处理成本不高的情况下,会反而降低性能。
  • 并行操作的开销:并行流的引入可能导致不必要的同步和线程切换,增加系统的负担。

3. 如何避免 Stream 代码变丑?

  虽然 Stream 能提高代码的简洁性,但为了避免上述问题,开发者应该注意以下几点:

3.1. 避免过度链式调用

  如果链式调用过长,可以考虑将其拆分成多个步骤,并为每个步骤添加有意义的变量名,这样可以使每个操作更加清晰。

改进示例:

java 复制代码
List<String> filteredStrings = strings.stream()
                                      .filter(s -> s.length() > 3)
                                      .collect(Collectors.toList());

List<String> upperCaseStrings = filteredStrings.stream()
                                               .map(String::toUpperCase)
                                               .collect(Collectors.toList());

3.2. 适当使用终止操作

  确保每个流的操作都有一个明确的终止操作,避免懒加载特性带来的困惑。常见的终止操作包括 collect()forEach()reduce() 等。

3.3. 提高代码可读性

  使用函数式编程时,尽量使每个操作简洁且有意义。避免复杂的 filter()map() 操作堆叠在一起,尽量将每个操作的意图表达得更加清晰。

3.4. 性能优化

  在使用并行流时,应该评估数据集的大小和操作的复杂性,确保并行流的引入确实能带来性能提升。

总结

  Stream 是 Java 中非常强大的工具,它能够使代码更加简洁、声明式且易于扩展。然而,在一些复杂的场景中,滥用 Stream 或者不当的使用方式可能会让代码变得难以理解,甚至影响性能。为了避免这些问题,开发者应该根据具体场景和需求合理使用 Stream,保持代码简洁且易于理解,所以说,任何事物都有两面性,适合的场景才是最好的!而不是任何场景。

... ...

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

... ...

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。

⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

相关推荐
YDS82940 分钟前
DeepSeek RAG&MCP + Agent智能体项目 —— 集成ELK日志管理系统和Prometheus监控系统
java·elk·ai·springboot·agent·prometheus·deepseek
骄马之死8 小时前
SpringMVC + SpringBoot 核心知识点总结
java·spring boot·后端
GoGeekBaird9 小时前
Anthropic技能"(Skills)的经验分享
后端
王码码20359 小时前
多台服务器怎么统一看状态?Beszel 轻量监控,搭起来不费事
运维·服务器·后端·安全·阿里云·接口·web
郑洁文9 小时前
基于Spring Boot的流浪动物救助网站
java·spring boot·后端·毕设·流浪动物救助
螺丝钉code10 小时前
JAVA项目 Claude code CLAUDE.md 到底应该怎么写
java·人工智能·claude code
指令集梦境11 小时前
Cursor + Spring Boot实战:从零写一个RESTful API
spring boot·后端·restful
摇滚侠11 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
VidDown11 小时前
Webhook 调试器:让第三方回调“原形毕露”
java·开发语言·javascript·编辑器·postman
码云之上11 小时前
聊聊如何设计一个高效、稳定的 Node.js 接入层
前端·后端·node.js