Java 8 流式编程

一、流是什么,为什么需要它

很多人学流的时候,第一个疑问就是:我有集合了,为什么还需要流?

要回答这个问题,先要理解集合和流的本质区别:

复制代码
集合:关注的是数据怎么存
流:  关注的是数据怎么处理

集合是一个容器,你把东西放进去,需要的时候取出来。而流完全不一样,流没有存储,它更像是一条传送带,数据在上面流动,你在传送带旁边安装各种处理装置,数据经过的时候自动被处理。

这个区别带来了一个非常重要的思维转变:以前你在想怎么操作集合,现在你只需要描述你想要什么结果。


二、命令式 vs 声明式,这才是流的灵魂

书里举了一个非常好的例子,生成5到20之间不重复的7个随机整数并排序。

传统写法(命令式):

复制代码
Random rand = new Random(47);
SortedSet<Integer> rints = new TreeSet<>();
while(rints.size() < 7) {
    int r = rand.nextInt(20);
    if(r < 5) continue;
    rints.add(r);
}

你必须仔细读完整段代码,才能弄明白它在干什么。为什么有个while循环?为什么要判断r<5?这些都是实现细节,和你真正想表达的意图混在一起,阅读负担很重。

流式写法(声明式):

复制代码
new Random(47)
    .ints(5, 20)
    .distinct()
    .limit(7)
    .sorted()
    .forEach(System.out::println);

这段代码几乎就是在用中文说话:生成5到20的随机数,去重,取7个,排序,打印。代码直接表达了意图,而不是实现步骤。

这就是声明式编程的魅力:说你想要什么,而不是说怎么做。


三、流的三种操作,必须牢记

流的所有操作分为三类,理解这三类是学流的基础:

第一类:创建流

流从哪里来?有很多来源:

从集合创建 ,这是最常见的方式,任何集合调用 .stream() 就能得到一个流。你的List、Set、Map都可以变成流。

从数组创建 ,通过 Arrays.stream() 把数组变成流。

直接创建 ,用 Stream.of() 直接把几个元素变成流,就像 Stream.of("张三","李四","王五")

无限流 ,这是流最神奇的地方之一。Stream.generate()Stream.iterate() 可以创建理论上无限长的流。比如用 Stream.generate() 不断生成随机数,用 Stream.iterate() 生成斐波那契数列。这在集合里是完全不可能的,你不可能创建一个无限长的ArrayList。

第二类:中间操作

中间操作是流的核心,它接收一个流,返回另一个流,所以可以链式调用,一个接一个串起来。

filter(过滤):给它一个条件,满足条件的元素留下,不满足的丢掉。就像筛子。

map(映射/转换):把每个元素转换成另一种形式。把字符串列表转成长度列表,把用户列表转成用户名列表,都是map干的事。

distinct(去重):去掉重复元素,不需要手动维护Set了。

sorted(排序):可以用默认排序,也可以传入自定义的比较器。

limit(截取):只取前N个元素,对无限流特别重要,有了limit才能从无限流里取有限个元素。

skip(跳过):跳过前N个元素,和limit配合可以实现分页效果。

peek(查看):这是个专门用来调试的操作,可以查看流中每个元素,但不改变它,就像在流水线上装了个摄像头。

flatMap(扁平化):这个稍微复杂,但非常重要。如果你的map操作产生的是一个流的流(每个元素变成了一个流),flatMap会把这些流都摊平,合并成一个流。就像把一箱箱苹果全部倒出来,变成一堆苹果。

第三类:终端操作

终端操作是流的终点,调用之后流就结束了,产生一个最终结果。

forEach:遍历每个元素,做某件事,没有返回值。

collect :把流收集成集合或其他形式,是最常用的终端操作。比如收集成List、Set,或者用 Collectors.joining() 把字符串流拼接成一个字符串。

sum/count/min/max:数学统计操作,对数值流特别有用。

findFirst/findAny:找到第一个或任意一个满足条件的元素。

noneMatch/anyMatch/allMatch:判断流中的元素是否满足某个条件。


四、流最容易被忽略的特性:懒加载

这是流的一个核心特性,很多人学了很久都没真正理解它。

流是懒加载的,中间操作在你调用终端操作之前,一个字节都不会执行。

复制代码
Stream.of(1, 2, 3, 4, 5)
    .filter(n -> {
        System.out.println("filter: " + n);
        return n > 3;
    })
    .map(n -> {
        System.out.println("map: " + n);
        return n * 2;
    });
// 此时什么都没打印!因为没有终端操作

只有加上终端操作,整条流水线才真正运转起来:

复制代码
.forEach(System.out::println);
// 现在才开始执行

懒加载带来了两个巨大好处:

第一,无限流成为可能。你可以创建一个理论上无限的流,因为它不会真的去生成无限个元素,只有在被需要的时候才生成。加上limit限制,整个计算就变得有限可控。

第二,性能优化 。流可以短路求值,比如 findFirst() 找到第一个满足条件的元素就立刻停止,不会傻傻地把剩余元素都处理一遍。


五、内部迭代 vs 外部迭代

这个概念书里特别强调,值得单独说清楚。

外部迭代就是我们传统的for循环写法,你自己控制迭代的过程:从哪开始、到哪结束、每次怎么走,全部由你来指挥。

内部迭代就是流的方式,你把迭代的控制权交出去,只告诉流"我要过滤"、"我要排序",具体怎么迭代是流内部的事,你看不到也不需要关心。

放弃迭代控制权听起来像是失去了什么,实际上却是获得了更多:

复制代码
你不控制迭代  →  流可以自由决定怎么迭代
              →  并行处理成为可能
              →  只需要改一行代码,流就可以用多核并行处理
              →  你写串行代码,享受并行性能

这就是为什么书里说"通过放弃对迭代过程的控制,可以把控制权交给并行化机制"。


六、flatMap,流里最难理解但最重要的操作

很多人学到flatMap就卡住了,觉得很抽象。其实用一个生活例子就能说清楚。

假设你有三个快递箱(三个流),每个箱子里有几件衣服(元素)。

map的结果:给你三个箱子,每个箱子里的衣服处理了一下,但还是三个箱子装着。

flatMap的结果:把三个箱子全部打开,所有衣服都倒出来,变成一堆衣服,箱子消失了。

复制代码
map:     [箱子1[衣服,衣服], 箱子2[衣服], 箱子3[衣服,衣服,衣服]]
flatMap: [衣服, 衣服, 衣服, 衣服, 衣服, 衣服]

在实际编程中,最典型的场景就是处理文件单词。每一行是一个元素,你想得到所有单词的流。用map把每行分割成单词数组,得到的是"数组的流"。用flatMap才能得到真正的"单词的流"。


七、流的创建方式全景

除了最常见的集合转流,还有几个特别有用的创建方式值得了解:

Stream.generate():接收一个Supplier,不断调用它生成元素。适合生成随机数、创建相同对象的序列等场景。因为是无限流,必须配合limit使用。

Stream.iterate():接收一个初始值和一个函数,每次把上一个结果传给函数得到下一个值。天然适合生成数列,比如斐波那契数列、等差数列。

IntStream.range():生成整数范围的流,可以完美替代传统for循环,让循环逻辑变成流式风格。

Files.lines():直接把文件的每一行变成流,处理文件再也不需要先把所有行读入内存。


八、一个完整的流使用场景

把所有知识串起来,看一个完整的例子:

想象你有一个订单列表,要找出所有金额大于1000元的订单,提取订单号,去重后排序,最后拼成一个字符串展示:

复制代码
订单列表
    → filter:只要金额>1000的          (中间操作,Predicate)
    → map:提取订单号                   (中间操作,Function)
    → distinct:去重                    (中间操作)
    → sorted:排序                      (中间操作)
    → collect(joining(",")):拼接字符串  (终端操作)

整个过程就像一条流水线,数据从左边进来,经过一道道工序,最终得到你想要的结果。没有临时变量,没有循环,代码即注释。


九、总结:流改变了什么

复制代码
改变了思维方式:
    以前:怎么操作数据(命令式)
    现在:想要什么结果(声明式)

改变了代码风格:
    以前:循环+判断+临时变量,逻辑分散
    现在:链式调用,逻辑集中,一眼看清意图

改变了性能可能性:
    以前:串行是默认,并行需要大量改造
    现在:parallel()一行代码,串行变并行

流的本质:
    不是新的数据结构
    是一种新的处理数据的方式
    是声明式编程思想在Java中的体现

学会用流,不只是学会了一个API,而是学会了一种新的思考和表达问题的方式。 当你开始习惯用流思考问题,你会发现代码变得更短、更清晰、更接近你真正想表达的意思。

相关推荐
肯戳加勾1 小时前
JAVA最常见的装箱/拆箱坑
java·后端
Memory_荒年1 小时前
ReentrantLock:AQS家的“锁二代”,但比 synchronized 更会“来事儿”
java·后端
巫山老妖1 小时前
OpenClaw 心跳机制实战:让 AI Agent 24 小时不停自主运行
java·前端
没有bug.的程序员1 小时前
低代码平台后端引擎:元数据驱动架构、插件化内核与 Java 扩展机制
java·低代码·架构·插件化·元数据·扩展机制
czhc11400756632 小时前
c# 312 事件 委托
开发语言·c#
懈尘2 小时前
【实战分享】智慧养老系统核心模块设计 —— 健康监测与自动紧急呼叫
java·后端·websocket·mysql·springboot·livekit
亚马逊云开发者2 小时前
写 Prompt 让 AI 出代码?Kiro 说你该先写 Spec
java
xyq20242 小时前
R 基础运算
开发语言
筱顾大牛2 小时前
点评项目---分布式锁
java·redis·分布式·缓存·idea