Java Lambda + Stream:让你的代码从 100 行变 10 行

Lambda + Stream 绝对是我用过的最值得的投资。它没有多难,但收益巨大——同样的逻辑,传统写法要 50 行,新写法 5 行搞定,可读性还更好。这篇博客,把压箱底的经验全掏出来,一次讲透。


一、为什么 Lambda + Stream 这么值得学?

先看一段"传统写法"的代码——这是无数 Java 项目的真实写照:

// 场景:找出所有年龄 > 18 的用户,按姓名排序,提取用户名组成列表
List<String> result = new ArrayList<>();
for (User user : users) {
    if (user.getAge() > 18) {
        result.add(user.getName());
    }
}
Collections.sort(result);
return result;

10 行代码,能干的事情就这么点。要是再加上"分组统计"、“求和”、"求平均"呢?for 循环能嵌套到天荒地老。

更痛苦的是,这段代码读起来像流水账——你需要从 for 开始一行行读,才能理解"到底在做什么"。但你的本意明明是"过滤+排序+收集",一句话就能说清楚。

范式转变

  • Java 8 之前:把"做什么"藏在"怎么做"里
  • Java 8 之后:把"做什么"写在明面上,剩下的交给框架

这就是 Lambda + Stream 要解决的核心问题——让代码更接近自然语言

📖 参考文档:Lambda Expressions - Oracle Java Tutorials


二、核心概念:把代码想象成"流水线"

2.1 函数式编程 vs 命令式编程

先打个比喻。

传统写法 = 工人一步步手搓产品:先拿料、再切割、再打磨、再组装……每一步都得你亲自指挥。

Lambda + Stream = 你只管下订单:“我要 100 个 A 产品”,流水线自动给你做好。

两者的核心区别:

维度 命令式(传统) 函数式(Lambda/Stream)
关注点 怎么做(how) 做什么(what)
代码结构 for/if 嵌套 链式调用
状态 大量可变变量 几乎无副作用
可读性 需要读完整段才能理解 一眼就能看出意图

2.2 三个核心概念

概念 1:函数式接口(Functional Interface)

只有一个抽象方法的接口(Single Abstract Method, SAM)。Lambda 的本质就是"函数式接口的实例"。

// 标记注解,编译器会校验"是否真的是函数式接口"
@FunctionalInterface
public interface Calculator {
    int calculate(int a, int b);  // 唯一抽象方法
}

为什么需要这个概念?因为 Java 是一门强类型语言——Lambda 虽然像函数,但它必须有一个"类型",而这个类型就是函数式接口。

概念 2:Lambda 表达式

Lambda = 函数式接口的简写。它把"匿名内部类"的样板代码全部砍掉。

// 1️⃣ 传统:匿名内部类(5 行)
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// 2️⃣ Lambda:去掉方法名和修饰符(1 行)
Runnable r2 = () -> System.out.println("Hello");
概念 3:Stream API

Stream = 数据流水线。它不存数据,只是把数据从一端搬运到另一端,途中做各种加工。

SQL:SELECT name FROM users WHERE age > 18 ORDER BY name
Java Stream:用户用 Java 语法描述同样的事情

// Java Stream 版"找出所有年龄 > 18 的用户,按姓名排序"
List<String> result = users.stream()
    .filter(u -> u.getAge() > 18)      // 过滤(WHERE)
    .map(User::getName)                 // 转换(SELECT)
    .sorted()                           // 排序(ORDER BY)
    .collect(Collectors.toList());      // 收集

一句话总结读这段代码,就像在读 SQL

📖 参考文档:java.util.stream (Java SE 8)


三、核心体系:一图看懂

在这里插入图片描述

3.1 四大核心函数式接口

Java 预置了 4 个最常用的函数式接口,记住它们能解决 80% 的场景:

[图片位置:四大函数式接口分类图(Function/Consumer/Supplier/Predicate 的关系)]

接口 方法签名 输入 输出 用途
Function<T, R> R apply(T t) T R 数据转换(map)
Consumer<T> void accept(T t) T 消费数据(forEach)
Supplier<T> T get() T 生产数据(工厂)
Predicate<T> boolean test(T t) T boolean 条件判断(filter)
// 1. Function:字符串转长度
Function<String, Integer> lengthFunc = String::length;
lengthFunc.apply("Hello");  // 5

// 2. Consumer:打印
Consumer<String> printer = System.out::println;
printer.accept("Hello");  // Hello

// 3. Supplier:生成随机数
Supplier<Double> random = Math::random;
random.get();  // 0.7234...

// 4. Predicate:判断正数
Predicate<Integer> isPositive = n -> n > 0;
isPositive.test(5);  // true

📖 参考文档:java.util.function (Java SE 8)

3.2 Stream 流水线流程

[图片位置:Stream 流水线流程图(数据源 → 中间操作链 → 终端操作 → 结果)]
Stream 操作分两大类:

类别 特点 常见方法
中间操作 懒执行,返回新 Stream,可链式 filtermapflatMapsorteddistinctlimitskip
终端操作 触发整个流水线执行 forEachcollectreducecountanyMatch

⚠️ 关键概念:没有终端操作时,整个流水线根本不会跑!这是很多初学者的盲区。

Stream 采用的是懒加载(Lazy Evaluation):只有遇到终端操作(terminal operation)时,中间操作才会被触发执行。

3.3 方法引用 4 种类型

方法引用是 Lambda 的"语法糖"——当 Lambda 体只是"调用一个已有方法"时,可以更简洁。

类型 语法 等价 Lambda
静态方法引用 Class::staticMethod (x) -> Class.staticMethod(x)
实例方法引用 instance::method (x) -> instance.method(x)
任意对象方法引用 Class::method (x, y) -> x.method(y)
构造器引用 Class::new () -> new Class()
// 静态方法引用
Function<String, Integer> parse = Integer::parseInt;

// 实例方法引用
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;

// 构造器引用
Supplier<ArrayList<String>> listSupplier = ArrayList::new;

四、实战代码:3 个完整示例

示例 1:传统写法 vs Lambda 演进

场景:启动一个线程打印 “Hello”

// 1️⃣ 传统写法:5 行
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
}).start();

// 2️⃣ Lambda 写法:3 行
new Thread(() -> System.out.println("Hello")).start();

// 3️⃣ 方法引用:3 行
new Thread(System.out::println).start();  // 注意:这不是 () -> println("Hello")

运行结果

Hello

示例 2:集合 → Stream 重构

场景:员工集合,找出薪资 > 10000 的,按部门分组,统计每个部门的人数。

2.1 传统写法
// 不用 Stream:全靠 for 循环和 Map
Map<String, Integer> result = new HashMap<>();
for (Employee emp : employees) {
    if (emp.getSalary() > 10000) {
        String dept = emp.getDepartment();
        if (result.containsKey(dept)) {
            result.put(dept, result.get(dept) + 1);
        } else {
            result.put(dept, 1);
        }
    }
}
return result;
2.2 Stream 写法(3 行)
// 用 Stream:链式调用,意图清晰
Map<String, Long> result = employees.stream()
    .filter(e -> e.getSalary() > 10000)   // 过滤高薪员工
    .collect(Collectors.groupingBy(        // 按部门分组
        Employee::getDepartment,
        Collectors.counting()              // 统计每组数量
    ));

运行结果

// 假设员工数据:
// - Alice, 研发部, 15000
// - Bob, 研发部, 12000
// - Charlie, 销售部, 8000
// - David, 销售部, 18000

// 输出:
// {研发部=2, 销售部=1}

收益对比

维度 传统写法 Stream 写法
代码行数 12 行 3 行
意图可读性 需要读完整段 一眼看穿
可维护性 加个条件要改 if 加个条件加个 filter
bug 概率 高(容易写错边界) 低(声明式)

示例 3:Optional 解决 NPE 噩梦

场景:根据用户 ID 获取用户名。如果用户不存在、地址不存在、城市不存在,都要优雅处理。

3.1 传统写法(NPE 重灾区)
// 传统写法:每一层都可能 NPE
String city;
try {
    User user = userRepository.findById(userId);
    if (user != null) {
        Address addr = user.getAddress();
        if (addr != null) {
            String c = addr.getCity();
            if (c != null) {
                city = c.toUpperCase();
            } else {
                city = "UNKNOWN";
            }
        } else {
            city = "UNKNOWN";
        }
    } else {
        city = "UNKNOWN";
    }
} catch (NullPointerException e) {
    city = "UNKNOWN";
}

20 行代码,3 个 if (xxx != null),还需要 try-catch。

3.2 Optional 写法(链式调用,1 行解决)
// Optional 链式调用:每一层都能优雅处理"无值"
String city = Optional.ofNullable(userId)
    .flatMap(id -> userRepository.findById(id))   // Optional<User>
    .map(User::getAddress)                         // Optional<Address>
    .map(Address::getCity)                         // Optional<String>
    .map(String::toUpperCase)                      // Optional<String>
    .orElse("UNKNOWN");                            // 默认值

1 行代码,零 NPE 风险,可读性爆表。

📖 参考文档:Optional (Java SE 8)

对比分析

维度 传统写法 Optional
代码量 20 行 1 行
NPE 风险 0
可读性 需要层层判断 链式清晰
性能 多次判 null 一次封装

五、避坑指南:8 条铁律

铁律 1:并行流不是银弹

// ❌ 数据量小时,并行流反而更慢
List<Integer> small = List.of(1, 2, 3, 4, 5);
small.parallelStream().filter(n -> n > 2).count();  // 慢!

// ✅ 数据量大 + 简单操作 + 易拆分的数据源,才考虑并行
List<Integer> big = IntStream.range(0, 1_000_000).boxed().toList();
big.parallelStream().filter(n -> n % 2 == 0).count();  // 提速明显

💡 判断标准:数据量 < 10000,不要用并行流。

铁律 2:Stream 禁止修改外部状态

// ❌ 灾难:多个线程同时修改 result
List<Integer> result = new ArrayList<>();
list.parallelStream().forEach(result::add);  // 数据错乱!

// ✅ 正确:用 collect
List<Integer> result = list.parallelStream().collect(Collectors.toList());

铁律 3:Optional 不要用作字段或参数

// ❌ 反模式 1:用作字段(影响序列化)
public class User {
    private Optional<String> name;  // 错误!
}

// ❌ 反模式 2:用作方法参数(失去类型约束)
public void setName(Optional<String> name) { }  // 错误!

// ✅ Optional 只适合用作方法返回值
public Optional<User> findByName(String name) { ... }

铁律 4:能方法引用就方法引用

// 不够简洁
list.stream().map(s -> s.toUpperCase()).collect(...);

// ✅ 方法引用更优雅
list.stream().map(String::toUpperCase).collect(...);

💡 判断标准:Lambda 体里只有一行方法调用 → 改方法引用。

铁律 5:数值操作用 mapToInt 避免装箱

// ❌ 每次都要把 int 装箱成 Integer
double avg = employees.stream()
    .map(Employee::getSalary)        // Stream<Integer>
    .mapToInt(Integer::intValue)     // 转 IntStream
    .average()
    .orElse(0);

// ✅ 直接 mapToInt
double avg = employees.stream()
    .mapToInt(Employee::getSalary)   // IntStream,零装箱
    .average()
    .orElse(0);

铁律 6:peek 只能用于调试

// ✅ 调试用:查看流水线中间结果
list.stream()
    .filter(s -> s.length() > 3)
    .peek(s -> System.out.println("after filter: " + s))  // 👀 调试
    .map(String::toUpperCase)
    .forEach(System.out::println);

⚠️ 不要用 peek 改外部状态,行为不可控。

铁律 7:Stream 只能消费一次

// ❌ Stream 是一次性的,用完就废
Stream<String> stream = list.stream();
stream.forEach(System.out::println);
stream.count();  // 报错:IllegalStateException: stream has already been operated upon

// ✅ 每次需要新流
list.stream().forEach(System.out::println);
list.stream().count();

铁律 8:Lambda 内不要修改外部变量

int sum = 0;
// ❌ 编译错误:Variable used in lambda should be final or effectively final
list.forEach(n -> sum += n);

// ✅ 正确:让 Lambda 不修改外部变量
int sum = list.stream().mapToInt(Integer::intValue).sum();

Java 8 强制要求:Lambda 捕获的外部变量必须是 final 或 effectively final。


六、总结:3 张速查表 + 1 句口诀

6.1 Lambda 选型速查表

场景 推荐写法
简单条件判断 (x) -> x > 0
数据转换 (x) -> x.toString()
已有方法可调用 优先用方法引用String::length
多行逻辑 { } 包裹

6.2 Stream 选型速查表

需求 推荐方法
过滤 filter
转换 map
一对多转换 flatMap
去重 distinct
排序 sorted
取前 N 个 limit
跳过前 N 个 skip
转 List collect(Collectors.toList())
分组 collect(Collectors.groupingBy(...))
求和 mapToInt(...).sum()
找第一个 findFirst()
是否包含 anyMatch(...)

6.3 Optional 选型速查表

需求 推荐方法
包装可能为 null 的值 Optional.ofNullable(x)
提供默认值 .orElse("default")
延迟提供默认值 .orElseGet(() -> ...)
无值抛异常 .orElseThrow(() -> ...)
链式转换 .map(...) / .flatMap(...)
条件过滤 .filter(...)
存在时执行 .ifPresent(...)

最后,感谢大家的观看,点赞,谢谢!

更多推荐