Skip to content

元数据卡

  • 前置知识:第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 类,实现一个接口,传递给调度器。

java
// 老办法:写一个新类
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 不能凭空出现,它需要一个"插座"——函数式接口

函数式接口就是只有一个抽象方法的接口。不多不少,就一个。

java
// 函数式接口:只有一个抽象方法
@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 标准库里的:

  • Runnablevoid run()
  • Callable<T>T call()
  • Comparator<T>int compare(T o1, T o2)

每一个都只有一个抽象方法。它们等着被 Lambda 插上。

第二幕:Lambda——把行为装进信封

好,你有接口了。以前你是怎么用的?

java
// 匿名内部类——Java 1.1 到 7 的写法
EventHandler handler = new EventHandler() {
    @Override
    public void handle(Event event) {
        System.out.println("事件发生: " + event.name());
    }
};

语言:Java 5+ 问题:6 行代码干了一件事。结构比内容还多。这就是"仪式感太重"。

Lambda 把上面这段话压缩成一行:

java
// Lambda 表达式
EventHandler handler = (event) -> System.out.println("事件发生: " + event.name());

语言:Java 8+ 如何运行

java
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
java
// 各种 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 自动捕获外部 finaleffectively 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

看个真实例子:

java
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 已经很短了,但有时你只是调用一个已有的方法。

java
// Lambda 写法
Consumer<String> printer = s -> System.out.println(s);

// 方法引用——更短!
Consumer<String> printer = System.out::println;

双冒号 :: 是方法引用操作符。它等于"这个地方放一个方法引用,等我需要调的时候再调"。

常见四种形式:

java
// 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 的时候你会写什么?

java
// 老办法——嵌套循环,临时变量,容易出错
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:

java
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 循环的控制变量,没有中间索引操作。每一步声明式地描述"我想做什么"而不是"怎么做"。

流操作分两类:

类别行为例子效果
中间操作返回新流,懒执行filtermapsortedlimitdistinct构建流水线,数据不动
终端操作触发执行,产生结果collectforEachcountreduceanyMatch真正取数据或消费
java
// 更多 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

java
// 🙅 编译错误
String prefix = "[消息]";
EventHandler h = event -> {
    prefix = "[紧急] " + prefix;   // ❌ prefix 被修改了!
    System.out.println(prefix);
};

Java 要求 Lambda 里使用的局部变量必须是 finaleffectively final(赋值后不再改变)。这是为了保护 Lambda 中变量引用的确定性——Lambda 可能稍后在另一个线程执行,变量可能已经变化。

解决方案:用一个数组或对象包装可变状态:

java
// ✅ 正确姿势
String[] prefix = {"[消息]"};
EventHandler h = event -> {
    prefix[0] = "[紧急]";  // 数组的引用没变,只是修改了内容
    System.out.println(prefix[0] + event.name());
};

陷阱二:Stream 只能消费一次

java
Stream<String> stream = messages.stream();
stream.forEach(System.out::println);
stream.count();   // 💥 IllegalStateException: stream has already been operated upon or closed

Stream 就像河里的水——流过去就流过去了。你不能复用同一条 Stream。每次操作都要新建:messages.stream()

🧭 本节仅做警示:并行流的线程安全问题是 Vol 9(并发与函数式编程)的内容。 初学阶段建议只在单线程中使用 Stream。

陷阱三:并行流的坑

java
// 🚨 简单粗暴加 .parallelStream()
list.parallelStream().forEach(item -> {
    // 操作共享变量!线程不安全!
    counter++;   // ❌ 竞态条件
});

parallelStream() 看起来好用,但共享可变状态 + 多线程 = 竞态条件。如果不理解线程安全,先只用 stream()

🏔 深入冒险

闭包(Closure)——Lambda 捕获的变量去哪了

当你在 Lambda 里使用了一个外部变量,这个 Lambda 就"捕获"了那个变量,形成一个闭包

java
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 声明),但也更容易踩坑(闭包绑定引用而非值):

python
funcs = []
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() 操作里修改了外部状态:

java
// 🙅 坏代码
List<String> log = new ArrayList<>();
users.stream()
    .map(user -> {
        log.add("处理用户: " + user.name());   // 副作用!
        return user.email();
    })
    .collect(Collectors.toList());

这样写的问题:

  1. 如果后面改成 parallelStream()ArrayList 不是线程安全的
  2. Stream 管道的核心思想是"无副作用"——你用 map 来转换,不应该在这里做日志
  3. 调试困难:管道中间步骤的副作用不可见

更好做法:

java
// ✅ 好代码——日志是另一个管道步骤
users.stream()
    .peek(user -> System.out.println("处理用户: " + user.name()))  // 明确为调试
    .map(User::email)
    .collect(Collectors.toList());

或更简单:把日志放到终端操作之前。

通关挑战

  • 🗡 热身(10 分钟,必做)
  1. 给你一个字符串列表,用 Stream 完成:
    • 过滤出长度大于 3 的
    • 全部转大写
    • 排序
    • 打印每个元素
  2. 把下面这个匿名内部类改写成 Lambda:
java
Runnable task = new Runnable() {
    @Override
    public void run() {
        System.out.println("任务执行中");
    }
};
  1. 把下面这个 Lambda 改写成方法引用:
java
Function<String, Integer> f = s -> Integer.parseInt(s);
  • 挑战(30 分钟,选做)
  1. Stream 数据报告:给定一个 Villager 列表(字段:name, age, village),用 Stream API 完成:

    • 找出所有来自"变量村"的村民
    • 按年龄排序
    • 只保留名字
    • 收集成 List<String>
    • 计算所有人的平均年龄
  2. 自定义函数式接口:写一个 Transformer<T, R> 接口(可以写 @FunctionalInterface),然后创建一个其数组,每个数组元素是一个不同的转换 Lambda。循环调用所有转换。

验收标准

  • 你能写出 Lambda 表达式(无参、单参、多参),并理解 -> 的语法含义
  • 你能解释"函数式接口"和 Lambda 的关系——插座与插头
  • 你能用方法引用替换简单的 Lambda:System.out::printlnString::lengthInteger::parseInt
  • 你能用 Stream API 完成"过滤-转换-排序-收集"四步流水线
  • 你能区分中间操作和终端操作,知道终端操作之前 Stream 不会真正执行

常见卡点

"Lambda 和匿名内部类一样吗?不一样。 Lambda 底层用 invokedynamic(invoke dynamic)指令实现,不是生成匿名类。性能更好,编译后表示更轻量。

"为什么 Lambda 里的 this 指向外部类,而匿名内部类里的 this 指向自己? 因为 Lambda 没有自己的作用域——它和包围它的代码共享同一个 this。匿名内部类是一个独立的类,它的 this 就是它自己的实例。这个区别在实际编码中经常导致混淆。

"Stream 和集合有什么区别?

  • 集合存储数据,Stream 不存储——它只是数据的"视图"或"管道"
  • 集合可以被多次遍历,Stream 只能消费一次
  • Stream 是懒执行的——中间操作只在终端操作触发时才真正计算

现在不需要理解

  • invokedynamic 指令的 JVM 实现细节
  • Spliterator 如何支撑并行流
  • Collector 的高级用法(groupingBypartitioningBy
  • 归约操作 reduce 的三种重载——单参数和三参数形式
  • Optional 类——它是 Stream API 的好搭档,但在更后面章节单独讲

旅人笔记

Lambda 把行为装进信封传给别人。 函数式接口是插座,Lambda 是插头。 方法引用让 Lambda 再短一步。 Stream 流水线 = filter → map → sorted → collect,让集合操作读起来像英语句子。 四个核心:Supplier 生产值,Consumer 消费值,Function 转换值,Predicate 判断值。

下一站预告

你已经在"变量村"学完了编程的骨架知识——变量、控制流、方法、类、继承、Lambda。

下一站,你会学到最常用的工具类:字符串怎么就不可变了?为什么不能直接拼 + 来造长字符串?枚举和 record 为什么能省那么多代码?这是你在新手村的最后一站——之后就要走进算法森林

Built with VitePress | Software Systems Atlas