元数据卡
- 前置知识:第5章(方法定义与调用)、第8章(接口)
- 预计时间:40 分钟
- 核心难度:
- 阅读模式: 高度专注
- 可选跳过:如果你只做简单 CRUD,这章内容接触较少;但如果想用 Java Stream API 或写整洁的事件驱动代码,这章必读
- 完成标志:能写出 Lambda 表达式和方法引用,能用 Stream API 做集合流水线操作
本章分层
- 必读:Lambda 表达式的基本语法、
list.stream().map()和filter()的简单流水线- 选读:方法引用、四大核心函数式接口(
Supplier/Consumer/Function/Predicate)- 深水区:闭包(closure)的概念、Stream 的惰性求值
可选进阶章:Lambda 和 Stream 是 Java 8 引入的进阶特性。 如果你是编程初学者,可以跳过本章直接进入第9章,后续再回来补。
本章不会要求你掌握
- 并行流(
parallelStream)及其线程安全问题——留到 Vol 9 并发深度卷- 闭包中 side effect 的管理——留到 Vol 9 函数式编程卷
你在哪
老陈见你继承多态都用利索了,丢给你一个更刁钻的问题。村口需要一套事件报警系统——每次有状况都要执行一串操作,你不想为每种状况都新建一个类。
你已经学会了类、继承、接口——封装了一套坚实的工具箱,用类把数据和操作打包在一起。
但有一个问题一直缠着你。
你写了一个事件处理系统,村口每次有状况就要执行"通知村长 + 记录日志 + 准备对应物资"——三个动作。你现在是怎么做的?每来一个新事件,就创建一个新的 EventHandler 类,实现一个接口,传递给调度器。
// 老办法:写一个新类
class FloodEventHandler implements EventHandler {
@Override
public void handle(Event event) {
notifyVillageChief(event);
logEvent(event);
prepareSandbags(event);
}
}三个事件 → 三个类。五个事件 → 五个类。
老陈师傅看你写类写到手酸,摇了摇头:"你就是造打铁炉子的——一锤一锤地敲。你知不知道有一种更快的方法,可以把一段行为像包裹一样直接递过去?"
他拿出一片羽毛,在上面写了几行字。
"这叫 Lambda。"
你的任务
旧方法的问题是:代码里充斥着只需要用一次的"中间人"类。你其实不在乎它叫什么类,你只在乎"当事件发生时,运行这一段代码"。
这一章要解决的问题:能不能把"一段行为"当成一个值——像传数字、传字符串一样——直接从一处传到另一处?
Java 8 用 Lambda 表达式回答了这个需求。Lambda 让你把函数当作参数传递。而 Stream API 则让你像流水线一样处理集合数据。当你习惯了这种写法,你会发现自己再也不想写 for 循环去过滤和变换列表了。
遭遇战 → 获得技能
第一幕:函数式接口——Lambda 的插座
Lambda 不能凭空出现,它需要一个"插座"——函数式接口。
函数式接口就是只有一个抽象方法的接口。不多不少,就一个。
// 函数式接口:只有一个抽象方法
@FunctionalInterface // 👈 注解(可选,但推荐)——编译器会检查
public interface EventHandler {
void handle(Event event);
}
// 这是函数式接口吗?
public interface Clickable {
void onClick();
default void logClick() { // ✅ 默认方法不算
System.out.println("clicked");
}
}
// 是的,只有一个抽象方法语言:Java 8+ 说明:@FunctionalInterface 不是必须的,但它和 @Override 一样是安全网——如果接口不小心加了第二个抽象方法,编译器会报错
你可能用了很多次函数式接口而不自知——看看 Java 标准库里的:
Runnable→void run()Callable<T>→T call()Comparator<T>→int compare(T o1, T o2)
每一个都只有一个抽象方法。它们等着被 Lambda 插上。
第二幕:Lambda——把行为装进信封
好,你有接口了。以前你是怎么用的?
// 匿名内部类——Java 1.1 到 7 的写法
EventHandler handler = new EventHandler() {
@Override
public void handle(Event event) {
System.out.println("事件发生: " + event.name());
}
};语言:Java 5+ 问题:6 行代码干了一件事。结构比内容还多。这就是"仪式感太重"。
Lambda 把上面这段话压缩成一行:
// Lambda 表达式
EventHandler handler = (event) -> System.out.println("事件发生: " + event.name());语言:Java 8+ 如何运行:
public class LambdaDemo {
public static void main(String[] args) {
EventHandler handler = (event) -> {
System.out.println("事件: " + event.name());
System.out.println("时间: " + event.time());
};
handler.handle(new Event("敌军来袭", System.currentTimeMillis()));
// 也可以直接传参
processEvent((event) -> System.out.println("处理: " + event.name()));
}
static void processEvent(EventHandler h) {
h.handle(new Event("深夜异动", System.currentTimeMillis()));
}
}
// 辅助类
record Event(String name, long time) {}预期输出:
事件: 敌军来袭
时间: 1719123456789
处理: 深夜异动你试试:改写成多行 Lambda(用花括号包住方法体),看看不同
Lambda 的语法拆解:
(参数列表) → { 方法体 }| 部分 | 说明 | 简化规则 |
|---|---|---|
(event) | 参数 | 单一参数可省略括号:event -> |
-> | Lambda 操作符 | 固定写法,不能省略 |
{ ... } | 方法体 | 单条语句可省略花括号和 return |
// 各种 Lambda 形式
// 无参数
Runnable r = () -> System.out.println("跑起来了");
// 一个参数,省略括号
Consumer<String> c = msg -> System.out.println(msg);
// 多个参数,不能省括号
Comparator<Integer> comp = (a, b) -> a - b;
// 多行语句,需要花括号和 return
Comparator<Integer> verbose = (a, b) -> {
System.out.println("比较中...");
return a - b;
};语言:Java 8+
Lambda 的行为就像一段代码的"引用"——你没有定义类、没有写方法声明、没有写 new——直接说"这段代码"并把它传出去。
Python 窗口
python# Python 的 lambda handler = lambda event: print(f"事件: {event['name']}") process_event(handler) # 甚至更灵活——Python 的 lambda 支持任意表达式 sort_by_name = lambda items: sorted(items, key=lambda x: x['name'])Python 的
lambda更简洁(不需要@FunctionalInterface),但也更弱——只能写单表达式,不能用多行语句。Java 的 Lambda 可以用花括号写任意多行代码。Python 的多行函数需要def。
C++ 窗口
cpp// C++ lambda(C++11+) auto handler = [](const Event& e) { cout << "事件: " << e.name << endl; }; // C++ lambda 可以捕获外部变量! string prefix = "[EVENT]"; auto logged = [prefix](const Event& e) { // 按值捕获 cout << prefix << e.name << endl; }; // 按引用捕获 auto mutable_handler = [&prefix](const Event& e) { prefix = "[IMPORTANT]"; // 修改外部变量!需要用 & 捕获 cout << prefix << e.name << endl; };C++ 的 lambda 最大的不同是捕获列表(
[]内的部分)——你可以决定外部变量是传值还是传引用进来。Java 的 Lambda 自动捕获外部final或effectively final的变量,不支持引用捕获(无法修改外部变量)。Python 则直接使用闭包作用域,不加区分。
第三幕:Java 四大函数式接口
你不需要自己定义所有的函数式接口。Java 8 在 java.util.function 包里提供了 40+ 个标准接口。但你只需要记住 四大天王:
| 接口 | 方法签名 | 用途 | 代码示意 |
|---|---|---|---|
Supplier<T> | T get() | 生产一个值 | () -> "hello" |
Consumer<T> | void accept(T) | 消费一个值 | s -> System.out.println(s) |
Function<T,R> | R apply(T) | 转换一个值 | s -> s.length() |
Predicate<T> | boolean test(T) | 判断真假 | s -> s.length() > 5 |
看个真实例子:
import java.util.function.*;
import java.util.List;
public class FunctionDemo {
public static void main(String[] args) {
List<String> messages = List.of("敌军", "天气", "喜报", "求救");
// Predicate——过滤
Predicate<String> isUrgent = msg -> msg.equals("敌军") || msg.equals("求救");
// Consumer——打印
Consumer<String> print = msg -> System.out.println("📨 " + msg);
// Function——转换
Function<String, String> prefix = msg -> "[消息] " + msg;
// Supplier——生成
Supplier<String> greeting = () -> "系统就绪";
// 用起来
messages.stream()
.filter(isUrgent) // 只保留紧急消息
.map(prefix) // 加前缀
.forEach(print); // 打印
System.out.println(greeting.get()); // 调用 Supplier
}
}语言:Java 8+ 预期输出:
📨 [消息] 敌军
📨 [消息] 求救
系统就绪第四幕:方法引用——更短的 Lambda
Lambda 已经很短了,但有时你只是调用一个已有的方法。
// Lambda 写法
Consumer<String> printer = s -> System.out.println(s);
// 方法引用——更短!
Consumer<String> printer = System.out::println;双冒号 :: 是方法引用操作符。它等于"这个地方放一个方法引用,等我需要调的时候再调"。
常见四种形式:
// 1. 静态方法引用:类名::staticMethod
Function<String, Integer> parser = Integer::parseInt;
// 等效:s -> Integer.parseInt(s)
// 2. 实例方法引用(特定对象):对象::method
String city = "变量村";
Supplier<Integer> len = city::length;
// 等效:() -> city.length()
// 3. 实例方法引用(任意对象):类名::method
Function<String, Integer> getLen = String::length;
// 等效:s -> s.length()
// 4. 构造方法引用:类名::new
Supplier<List<String>> listMaker = ArrayList::new;
// 等效:() -> new ArrayList<>()语言:Java 8+ 预期输出:Integer.parseInt("42") → 42;"hello".length() → 5 你试试:用 String::toUpperCase 创建一个 Function<String, String>,传入"hello"看看输出
方法引用让代码读起来更像"我要在这里做这个操作"而不是"我传一个函数然后调用那个函数"——语法噪音降到最低。
第五幕:Stream API——集合流水线
现在到了重头戏。
想象你有一份村民名单,要找出所有年龄大于 18 岁的,按名字排序,取出前 5 个,统一加上户籍前缀。没有 Stream 的时候你会写什么?
// 老办法——嵌套循环,临时变量,容易出错
List<String> result = new ArrayList<>();
for (Villager v : villagers) {
if (v.age() > 18) {
result.add("[变量村] " + v.name());
}
}
Collections.sort(result);
List<String> top5 = result.subList(0, Math.min(5, result.size()));语言:Java 5+(改进型 for 循环)
现在用 Stream:
import java.util.*;
import java.util.stream.*;
List<String> result = villagers.stream()
.filter(v -> v.age() > 18) // 过滤
.map(v -> "[变量村] " + v.name()) // 转换
.sorted() // 排序
.limit(5) // 取前5
.collect(Collectors.toList()); // 收成列表语言:Java 8+ 预期输出:一个最多包含 5 条加了前缀的姓名字符串的列表
看明白了吗?没有临时列表,没有 for 循环的控制变量,没有中间索引操作。每一步声明式地描述"我想做什么"而不是"怎么做"。
流操作分两类:
| 类别 | 行为 | 例子 | 效果 |
|---|---|---|---|
| 中间操作 | 返回新流,懒执行 | filter、map、sorted、limit、distinct | 构建流水线,数据不动 |
| 终端操作 | 触发执行,产生结果 | collect、forEach、count、reduce、anyMatch | 真正取数据或消费 |
// 更多 Stream 操作
List<Integer> numbers = List.of(3, 1, 4, 1, 5, 9, 2, 6);
// distinct + sorted + count
long count = numbers.stream()
.distinct() // 去重 → [3,1,4,5,9,2,6]
.sorted() // 排序 → [1,2,3,4,5,6,9]
.count(); // 终端操作 → 7
System.out.println(count);
// anyMatch / allMatch / noneMatch
boolean hasEven = numbers.stream().anyMatch(n -> n % 2 == 0); // true
boolean allPositive = numbers.stream().allMatch(n -> n > 0); // true
// reduce——自定"折叠"操作
int sum = numbers.stream().reduce(0, (a, b) -> a + b); // 求和
int product = numbers.stream().reduce(1, (a, b) -> a * b); // 求积语言:Java 8+ 预期输出:7 → true → true → 31 → 6480
常见陷阱
陷阱一:Lambda 里的变量必须是 final 或 effectively final
// 🙅 编译错误
String prefix = "[消息]";
EventHandler h = event -> {
prefix = "[紧急] " + prefix; // ❌ prefix 被修改了!
System.out.println(prefix);
};Java 要求 Lambda 里使用的局部变量必须是 final 或 effectively final(赋值后不再改变)。这是为了保护 Lambda 中变量引用的确定性——Lambda 可能稍后在另一个线程执行,变量可能已经变化。
解决方案:用一个数组或对象包装可变状态:
// ✅ 正确姿势
String[] prefix = {"[消息]"};
EventHandler h = event -> {
prefix[0] = "[紧急]"; // 数组的引用没变,只是修改了内容
System.out.println(prefix[0] + event.name());
};陷阱二:Stream 只能消费一次
Stream<String> stream = messages.stream();
stream.forEach(System.out::println);
stream.count(); // 💥 IllegalStateException: stream has already been operated upon or closedStream 就像河里的水——流过去就流过去了。你不能复用同一条 Stream。每次操作都要新建:messages.stream()。
🧭 本节仅做警示:并行流的线程安全问题是 Vol 9(并发与函数式编程)的内容。 初学阶段建议只在单线程中使用 Stream。
陷阱三:并行流的坑
// 🚨 简单粗暴加 .parallelStream()
list.parallelStream().forEach(item -> {
// 操作共享变量!线程不安全!
counter++; // ❌ 竞态条件
});parallelStream() 看起来好用,但共享可变状态 + 多线程 = 竞态条件。如果不理解线程安全,先只用 stream()。
🏔 深入冒险
闭包(Closure)——Lambda 捕获的变量去哪了
当你在 Lambda 里使用了一个外部变量,这个 Lambda 就"捕获"了那个变量,形成一个闭包。
public static void main(String[] args) {
String villageName = "变量村"; // 👈 捕获的变量
Runnable announce = () -> {
System.out.println("来自 " + villageName + " 的消息!");
};
// 哪怕后面再改也不会影响——Java 不允许改(effectively final)
// villageName = "算法森林"; // ❌ 编译错误
announce.run(); // ✅ 输出:来自 变量村 的消息!
}闭包的核心:Lambda 不只记住了方法体,还记住了它创建时的"环境"——它把捕获的变量"打包"进自己内部了。这使得 Lambda 在哪里都可以携带上下文。
与 Python / C++ 对比
Python 的闭包更灵活(可以修改捕获变量,用
nonlocal声明),但也更容易踩坑(闭包绑定引用而非值):pythonfuncs = [] for i in range(3): funcs.append(lambda: i) # 闭包陷阱! print(funcs[0]()) # 输出 2,不是 0!C++ 的 Lambda 捕获机制最明确——你必须声明捕获方式(
[=]值捕获,[&]引用捕获,[this]捕获当前对象),没有隐式捕获。
常见陷阱
🧭 Side effect 管理:Stream 管道中副作用的正确处理属于函数式编程的高级话题, 在 Vol 9 中有系统讨论。初学阶段只需记住"不要在 map 中修改外部变量"。
真实故事:Stream 里用 side effect(副作用)
我见过一个同事在 Java Stream 的 .map() 操作里修改了外部状态:
// 🙅 坏代码
List<String> log = new ArrayList<>();
users.stream()
.map(user -> {
log.add("处理用户: " + user.name()); // 副作用!
return user.email();
})
.collect(Collectors.toList());这样写的问题:
- 如果后面改成
parallelStream(),ArrayList不是线程安全的 - Stream 管道的核心思想是"无副作用"——你用 map 来转换,不应该在这里做日志
- 调试困难:管道中间步骤的副作用不可见
更好做法:
// ✅ 好代码——日志是另一个管道步骤
users.stream()
.peek(user -> System.out.println("处理用户: " + user.name())) // 明确为调试
.map(User::email)
.collect(Collectors.toList());或更简单:把日志放到终端操作之前。
通关挑战
- 🗡 热身(10 分钟,必做)
- 给你一个字符串列表,用 Stream 完成:
- 过滤出长度大于 3 的
- 全部转大写
- 排序
- 打印每个元素
- 把下面这个匿名内部类改写成 Lambda:
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("任务执行中");
}
};- 把下面这个 Lambda 改写成方法引用:
Function<String, Integer> f = s -> Integer.parseInt(s);- 挑战(30 分钟,选做)
Stream 数据报告:给定一个
Villager列表(字段:name, age, village),用 Stream API 完成:- 找出所有来自"变量村"的村民
- 按年龄排序
- 只保留名字
- 收集成
List<String> - 计算所有人的平均年龄
自定义函数式接口:写一个
Transformer<T, R>接口(可以写@FunctionalInterface),然后创建一个其数组,每个数组元素是一个不同的转换 Lambda。循环调用所有转换。
验收标准
- 你能写出 Lambda 表达式(无参、单参、多参),并理解
->的语法含义 - 你能解释"函数式接口"和 Lambda 的关系——插座与插头
- 你能用方法引用替换简单的 Lambda:
System.out::println、String::length、Integer::parseInt - 你能用 Stream API 完成"过滤-转换-排序-收集"四步流水线
- 你能区分中间操作和终端操作,知道终端操作之前 Stream 不会真正执行
常见卡点
"Lambda 和匿名内部类一样吗?不一样。 Lambda 底层用 invokedynamic(invoke dynamic)指令实现,不是生成匿名类。性能更好,编译后表示更轻量。
"为什么 Lambda 里的 this 指向外部类,而匿名内部类里的 this 指向自己? 因为 Lambda 没有自己的作用域——它和包围它的代码共享同一个 this。匿名内部类是一个独立的类,它的 this 就是它自己的实例。这个区别在实际编码中经常导致混淆。
"Stream 和集合有什么区别?
- 集合存储数据,Stream 不存储——它只是数据的"视图"或"管道"
- 集合可以被多次遍历,Stream 只能消费一次
- Stream 是懒执行的——中间操作只在终端操作触发时才真正计算
现在不需要理解
invokedynamic指令的 JVM 实现细节Spliterator如何支撑并行流- Collector 的高级用法(
groupingBy、partitioningBy) - 归约操作
reduce的三种重载——单参数和三参数形式 - Optional 类——它是 Stream API 的好搭档,但在更后面章节单独讲
旅人笔记
Lambda 把行为装进信封传给别人。 函数式接口是插座,Lambda 是插头。 方法引用让 Lambda 再短一步。 Stream 流水线 = filter → map → sorted → collect,让集合操作读起来像英语句子。 四个核心:Supplier 生产值,Consumer 消费值,Function 转换值,Predicate 判断值。
→ 下一站预告
你已经在"变量村"学完了编程的骨架知识——变量、控制流、方法、类、继承、Lambda。
下一站,你会学到最常用的工具类:字符串怎么就不可变了?为什么不能直接拼 + 来造长字符串?枚举和 record 为什么能省那么多代码?这是你在新手村的最后一站——之后就要走进算法森林。