Java 进阶:用 Lambda 和 Stream 实现代码“减肥”!!!
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 流水线流程图(数据源 → 中间操作链 → 终端操作 → 结果)]](https://i-blog.csdnimg.cn/direct/9490b0767b80480bbd7be006f2f9444d.png)
Stream 操作分两大类:
| 类别 | 特点 | 常见方法 |
|---|---|---|
| 中间操作 | 懒执行,返回新 Stream,可链式 | filter、map、flatMap、sorted、distinct、limit、skip |
| 终端操作 | 触发整个流水线执行 | forEach、collect、reduce、count、anyMatch |
⚠️ 关键概念:没有终端操作时,整个流水线根本不会跑!这是很多初学者的盲区。
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(...) |
最后,感谢大家的观看,点赞,谢谢!
更多推荐


所有评论(0)