Skip to content

Lambda 表达式 ​

S1-025必学进阶约 90 分钟JDK 21编程语言基础 · 函数式与 Stream

1. 本节定位 ​

Lambda 让"函数"可以像值一样传递:list.removeIf(name -> name.length() > 5) 里的 name -> name.length() > 5 就是一个函数。

它改变了 Java 的写法——《Stream API》 的每个操作都建立在 Lambda 之上,《Optional》 与并发工具同理。本节把 Lambda 的语法、方法引用与变量捕获讲透。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出函数式接口的定义,并写出自定义函数式接口;
  • 熟练写 Lambda 的三种形式(表达式、代码块、方法引用);
  • 用 Function、Consumer、Supplier、Predicate 四个内置接口表达常见意图;
  • 写出四种方法引用,并知道它们分别对应什么 Lambda;
  • 解释"Lambda 只能捕获 effectively final 的变量"及其原因;
  • 说清 Lambda 与匿名内部类在 this 语义上的区别。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

看一个"按字符串长度排序"的需求,三种写法:

java
// Java 8 之前:匿名内部类
names.sort(new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
});

// Lambda
names.sort((a, b) -> Integer.compare(a.length(), b.length()));

// 方法引用 + 比较器工厂
names.sort(Comparator.comparingInt(String::length));

从 6 行到 1 行。Lambda 的价值不是"少打字",而是让"传递行为"这件事变得自然——这正是 Stream、Optional、并发工具能设计成现在这样的前提。

5. 核心概念 ​

5.1 函数式接口:Lambda 的落脚点 ​

函数式接口 = 只有一个抽象方法的接口(可以有默认方法与静态方法)。

java
@FunctionalInterface          // 可选,但能帮编译器检查
interface Calculator {
    long calculate(long a, long b);
}

Calculator add = (a, b) -> a + b;      // Lambda 就是它的实例

@FunctionalInterface 的作用:如果接口有零个或两个以上抽象方法,编译直接失败。它是一道防错栏。

为什么必须依附接口:Java 没有独立的"函数类型",Lambda 必须对应某个接口类型。编译器根据上下文推断是哪个接口——这叫"目标类型"。

5.2 Lambda 的三种写法 ​

java
// 1. 表达式形式:单行,自动 return
Calculator add = (a, b) -> a + b;

// 2. 代码块形式:多行,必须显式 return
Calculator multiply = (a, b) -> {
    long result = a * b;
    return result;
};

// 3. 方法引用形式:直接复用已有方法
Function<String, Integer> length = String::length;

参数写法的简化规则:

情况写法
参数类型可推断省略类型:(a, b) -> a + b
只有一个参数可省略括号:name -> name.length()
没有参数写空括号:() -> "值"

5.3 四个内置函数式接口 ​

java.util.function 包里已经准备好了绝大多数需要:

接口签名含义典型用途
Function<T, R>R apply(T)有进有出转换:String::length
Consumer<T>void accept(T)只进不出打印、写入、累加
Supplier<T>T get()不进只出延迟创建、提供默认值
Predicate<T>boolean test(T)判断过滤条件

扩展接口(需要两个参数或特殊形式时):

接口签名场景
BiFunction<T, U, R>R apply(T, U)两个入参的转换
UnaryOperator<T>T apply(T)同类型进出(如 toUpperCase)
BinaryOperator<T>T apply(T, T)两个同类型进、一个出(如 Integer::max)
IntFunction / ToIntFunction 等基本类型特化避免装箱开销

实践建议:优先用内置接口,而不是每个需求都自定义一个。只有当语义特别明确、或者参数超过两个时,自定义才值得(如 Calculator)。

5.4 方法引用的四种形式 ​

java
// 1. 静态方法引用:类名::静态方法
Function<String, Integer> parseInt = Integer::parseInt;

// 2. 实例方法引用(第一个参数作为调用者):类名::实例方法
Function<String, Integer> length = String::length;

// 3. 对象方法引用:对象::实例方法
String prefix = "编号-";
Function<String, String> concat = prefix::concat;

// 4. 构造方法引用:类名::new
Supplier<List<String>> newList = ArrayList::new;

方法引用的判断方法:把它"还原"成 Lambda 看是否说得通。

java
String::length         →  str -> str.length()
System.out::println    →  x -> System.out.println(x)
ArrayList::new         →  () -> new ArrayList<>()
Math::max              →  (a, b) -> Math.max(a, b)

注意第 2 种最易混:String::length 里 String 是参数的类型,不是调用者。它的 Lambda 形式是"参数作为调用者"。

5.5 变量捕获:effectively final ​

java
int base = 100;
Calculator addBase = (a, b) -> a + b + base;      // 可以
base = 200;                                        // 编译错误:base 必须是 final 或等效 final

规则:Lambda 可以读取外层的局部变量,但这些变量必须是 final 或"事实上没有再次赋值"(effectively final)。

为什么限制这么严:Lambda 可能在另一个线程、另一个时刻执行,如果允许修改局部变量,就会出现"变量存在于栈上但函数已经跑到了别处"的矛盾。Java 选择直接禁止,而不是引入闭包捕获的复杂性。

类字段不受此限制:

java
private int counter;                               // 字段
Runnable task = () -> counter++;                   // 可以(操作的是 this.counter)

5.6 Lambda 与匿名内部类的区别 ​

Lambda匿名内部类
this 的含义外层实例的 this自身实例的 this
能否访问外层局部变量只能读 effect-final同样只能读
代码量少多
适用函数式接口需要抽象类或非函数式接口时
java
public class Demo {
    String name = "外层";

    void run() {
        Supplier<String> lambda = () -> this.name;          // this 是 Demo 实例
        Supplier<String> anonymous = new Supplier<>() {
            @Override
            public String get() {
                return this.toString();                     // this 是匿名类实例
            }
        };
    }
}

实践结论:能用 Lambda 就用 Lambda;只有需要实现抽象类、或需要自己的 this 时才用匿名内部类。

6. 递进示例 ​

6.1 最小示例:把函数当参数传 ​

java
static long apply(Calculator calculator, long a, long b) {
    return calculator.calculate(a, b);
}

apply((a, b) -> a + b, 10, 3);        // 13
apply((a, b) -> a - b, 10, 3);        // 7

同一个方法,传不同的行为——这是"把函数作为参数"的核心价值。

6.2 业务示例:集合操作 ​

java
List<String> names = new ArrayList<>(List.of("dave", "ALICE", "cherry", "bob"));
names.removeIf(name -> name.length() > 5);          // 去掉太长的
names.replaceAll(String::toUpperCase);              // 全部转大写
names.sort(Comparator.naturalOrder());              // 排序

三行代码完成过去需要循环 + 判断 + 临时集合的工作,且意图更明确:每一行都在说"做什么",而不是"怎么做"。

6.3 反例:Lambda 用过头 ​

java
// 难看:嵌套太深
list.stream()
    .filter(x -> x.getStatus() != null && x.getStatus().equals("PAID") && x.getAmount() > 0)
    .map(x -> x.getItems().stream().map(i -> i.getPrice() * i.getQuantity()).reduce(0L, Long::sum))
    .forEach(x -> System.out.println(x));

修复:把复杂 Lambda 提成方法,用方法引用:

java
list.stream()
    .filter(this::isPaidOrder)
    .map(this::calculateTotal)
    .forEach(System.out::println);

判断标准:Lambda 体超过 3 行,或需要注释才能看懂,就该提成方法。 方法名本身是最好的注释。

7. 完整可运行代码 ​

文件:labs/functional/src/main/java/cn/kb/learn/functional/LambdaLesson.java

java
package cn.kb.learn.functional;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;

public class LambdaLesson {
    /** 自定义函数式接口:只有一个抽象方法 */
    @FunctionalInterface
    interface Calculator {
        long calculate(long a, long b);
    }

    static long apply(Calculator calculator, long a, long b) {
        return calculator.calculate(a, b);
    }

    public static void main(String[] args) {
        // 1. 匿名内部类 vs Lambda:本质相同
        Calculator anonymous = new Calculator() {
            @Override
            public long calculate(long a, long b) {
                return a + b;
            }
        };
        Calculator lambda = (a, b) -> a + b;
        System.out.println("匿名内部类: " + anonymous.calculate(3, 5));
        System.out.println("Lambda: " + lambda.calculate(3, 5));
        System.out.println("Lambda 也是接口实例: " + (lambda instanceof Calculator));

        // 2. Lambda 的三种写法
        Calculator add = (a, b) -> a + b;
        Calculator subtract = (a, b) -> a - b;
        Calculator multiply = (a, b) -> {
            long result = a * b;
            return result;
        };
        System.out.println("加法: " + apply(add, 10, 3));
        System.out.println("减法: " + apply(subtract, 10, 3));
        System.out.println("乘法(带代码块): " + apply(multiply, 10, 3));

        // 3. 四种内置函数式接口
        Function<String, Integer> length = String::length;
        System.out.println("Function(有进有出): " + length.apply("知识库"));

        Consumer<String> printer = text -> System.out.println("Consumer(只进不出): " + text);
        printer.accept("Hello Lambda");

        Supplier<String> supplier = () -> "由 Supplier 提供(不进只出)";
        System.out.println("Supplier: " + supplier.get());

        Predicate<Integer> isEven = number -> number % 2 == 0;
        System.out.println("Predicate(返回布尔): " + isEven.test(4) + " / 取反后: " + isEven.negate().test(4));

        BiFunction<Integer, Integer, Integer> max = Math::max;
        System.out.println("BiFunction(两个入参): " + max.apply(3, 9));

        // 4. 方法引用的四种形式
        Function<String, Integer> staticRef = Integer::parseInt;
        Function<String, Integer> instanceRef = String::length;
        String prefix = "编号-";
        Function<String, String> objectRef = prefix::concat;
        Supplier<List<String>> constructorRef = ArrayList::new;
        System.out.println("静态方法引用: " + staticRef.apply("42"));
        System.out.println("实例方法引用: " + instanceRef.apply("abcd"));
        System.out.println("对象方法引用: " + objectRef.apply("A1"));
        System.out.println("构造方法引用: " + constructorRef.get().getClass().getSimpleName());

        // 5. 变量捕获:只能捕获 effectively final 的局部变量
        int base = 100;
        Calculator addBase = (a, b) -> a + b + base;
        System.out.println("捕获局部变量 base=" + base + " 的结果: " + apply(addBase, 1, 2));

        // 6. 集合操作中的 Lambda
        List<String> names = new ArrayList<>(List.of("dave", "ALICE", "cherry", "bob"));
        names.removeIf(name -> name.length() > 5);
        System.out.println("removeIf(去掉长度大于 5 的): " + names);
        names.replaceAll(String::toUpperCase);
        System.out.println("replaceAll(转大写): " + names);
        names.sort(Comparator.naturalOrder());
        System.out.println("sort(自然序): " + names);
    }
}

运行命令:

powershell
cd labs
./mvnw -pl functional compile
java -cp functional/target/classes cn.kb.learn.functional.LambdaLesson

预期输出:

text
匿名内部类: 8
Lambda: 8
Lambda 也是接口实例: true
加法: 13
减法: 7
乘法(带代码块): 30
Function(有进有出): 3
Consumer(只进不出): Hello Lambda
Supplier: 由 Supplier 提供(不进只出)
Predicate(返回布尔): true / 取反后: false
BiFunction(两个入参): 9
静态方法引用: 42
实例方法引用: 4
对象方法引用: 编号-A1
构造方法引用: ArrayList
捕获局部变量 base=100 的结果: 103
removeIf(去掉长度大于 5 的): [dave, ALICE, bob]
replaceAll(转大写): [DAVE, ALICE, BOB]
sort(自然序): [ALICE, BOB, DAVE]

最后三行值得对照:removeIf 按长度过滤、replaceAll 转换、sort 排序——同一个集合,三次不同的行为注入,这就是 Lambda 的日常用法。

8. Debug 观察点 ​

  1. 在 apply(add, 10, 3) 处断点并按 F7 进入,观察 Lambda 内部如何执行——调试器里它会显示成一个类似 LambdaLesson.lambda$main$0 的方法名,说明 Lambda 编译后仍然是方法;
  2. 在 lambda instanceof Calculator 处断点,展开 lambda 对象,看到它的类型是 LambdaLesson$$Lambda$...——编译器生成的实现类;
  3. 在 names.removeIf(name -> name.length() > 5) 处 F7 进入 ArrayList.removeIf,观察它内部用 Predicate.test 遍历判断;
  4. 把 int base = 100; 后面加上 base = 200;,观察编译错误("从 lambda 表达式引用的本地变量必须是 final 或实际上的 final")。

9. 常见错误 ​

错误 1:捕获了会变化的局部变量 ​

java
int count = 0;
list.forEach(item -> count++);        // 编译错误:count 不是 effectively final

修复:改用可变容器(但不推荐)、或改用流式归约:

java
long count = list.stream().filter(...).count();

不要为了绕过限制而写 int[] count = {0}; list.forEach(x -> count[0]++);——这是反模式,掩盖了"应该用流式操作"的事实。

错误 2:代码块形式忘记 return ​

java
Calculator multiply = (a, b) -> {
    a * b;                            // 编译错误:缺少 return
};

错误 3:Lambda 里的 this 不是内部类实例 ​

java
class Demo {
    String name = "外层";

    Supplier<String> getter() {
        return () -> this.name;        // this 是 Demo 实例,不是 Supplier 实例
    }
}

这与匿名内部类的行为不同(5.6 节)。

错误 4:用方法引用时参数顺序不对 ​

java
BiFunction<String, String, Boolean> startsWith = String::startsWith;
// 等价于 (a, b) -> a.startsWith(b),不是 b.startsWith(a)

修复:写方法引用前先"还原"成 Lambda 确认。

错误 5:为了简短牺牲可读性 ​

见 6.3 节。记住:Lambda 的目标是表达力,不是字符数。

10. 动手实验 ​

目标:用 Lambda 重构一段命令式代码。

步骤:

  1. 运行第 7 节代码,核对输出;
  2. 写一个方法 List<String> filterAndTransform(List<String> source, Predicate<String> filter, Function<String, String> mapper):先过滤再转换(不修改入参);
  3. 用它完成:把 ["apple", "fig", "banana", "kiwi"] 中长度大于 3 的转成大写并返回;
  4. 用 Consumer 写一个方法 void forEachWithIndex(List<String> list, BiConsumer<Integer, String> action):带下标遍历;
  5. 把第 2、4 步的方法各用一次 Lambda 调用与一次方法引用调用,对比两种写法的适用场景。

验收方式:能说明"什么时候用 Lambda、什么时候用方法引用"(提示:方法引用更简洁但可读性依赖方法名)。

扩展任务:自定义一个函数式接口 Validator<T>(List<String> validate(T target)),用它表达"校验规则",并写一个 validateAll(T target, List<Validator<T>> validators) 把多个规则组合起来——体会"把行为当数据"的设计方式。

11. 练习题 ​

基础题

  1. 判断:Lambda 可以实现任何接口。(对 / 错)
  2. 填空:函数式接口是只有 ______ 个抽象方法的接口;Function 的抽象方法是 ______。
  3. 简答:Lambda 为什么不能修改外层的局部变量?

代码阅读题

java
List<String> list = new ArrayList<>(List.of("a", "bb", "ccc"));
list.removeIf(s -> s.length() == 2);
list.replaceAll(String::toUpperCase);
System.out.println(list);

Function<String, Integer> f1 = String::length;
Function<String, Integer> f2 = s -> s.length();
System.out.println(f1.apply("test") + " / " + f2.apply("test"));
  1. 输出是什么?f1 与 f2 有区别吗?

编程题

  1. 写一个方法 long reduce(List<Integer> numbers, BinaryOperator<Integer> operator, int identity):
    • 用 identity 作为初始值,依次把每个元素与累积值交给 operator;
    • 用它对 [1,2,3,4] 分别求"求和"与"求最大值";
    • 说明 identity 在求最大值时应该取什么值。

综合任务

  1. 设计一个"数据处理管道":
    • 方法 List<String> process(List<String> source, List<Function<String, String>> steps):把多个转换步骤依次应用到每个元素;
    • 支持任意组合:转小写、去空白、截断到 10 个字符、加前缀;
    • 用该方法组装两条不同的管道,输出不同结果;
    • 说明:这种"把步骤当数据"的设计有什么好处?(提示:责任链模式)
分级提示

提示 1(方向):第 4 题的两个方法引用与 Lambda 完全等价,输出相同。

提示 2(关键点):第 5 题求最大值的 identity 应该取 Integer.MIN_VALUE(或列表最小值),因为累积的起点不能比任何元素大。

提示 3(接近答案):第 6 题的 process 对每个元素依次应用 steps 里的函数,可以用循环也可以再叠一层 reduce。

12. 参考答案 ​

基础题
  1. 错。Lambda 只能实现函数式接口(恰好一个抽象方法)。有多个抽象方法的接口必须用匿名类或实现类。
  2. 一;R apply(T t)。
  3. 因为 Lambda 可能在别的线程、别的时刻执行,而局部变量存在于栈上,方法返回后就消失了。如果允许修改,就会出现"变量生命周期与函数执行时机不匹配"的问题。Java 直接要求捕获的变量是 final 或 effectively final,以避免这种复杂性。
代码阅读题(第 4 题)
text
[A, CCC]
4 / 4
  • removeIf 去掉了 "bb",剩下 ["a", "ccc"];
  • replaceAll(String::toUpperCase) 转成大写得 [A, CCC];
  • f1.apply("test") 与 f2.apply("test") 结果相同(都是 4)。

f1 是方法引用、f2 是等价 Lambda,编译后的行为完全一致。方法引用只是更简洁的写法,不涉及性能差异。

编程题(第 5 题)
java
package cn.kb.learn.functional;

import java.util.List;
import java.util.function.BinaryOperator;

public class Reducer {
    static long reduce(List<Integer> numbers, BinaryOperator<Long> operator, long identity) {
        long result = identity;
        for (Integer number : numbers) {
            result = operator.apply(result, (long) number);
        }
        return result;
    }

    public static void main(String[] args) {
        List<Integer> numbers = List.of(1, 2, 3, 4);
        System.out.println("求和: " + reduce(numbers, Long::sum, 0L));
        System.out.println("求最大值: " + reduce(numbers, Math::max, Long.MIN_VALUE));
    }
}

identity 的选法:identity 必须满足"与任何元素运算都不改变结果"这一性质(单位元)。

  • 求和:0(0 + x = x);
  • 求最大值:Long.MIN_VALUE(任何值与它取 max 都是那个值本身)。

如果求最大值的 identity 填 0,那么全负数列表的结果会错误地返回 0——这是"约简初始值"最容易出错的地方。

预期输出:

text
求和: 10
求最大值: 4
综合任务(第 6 题)
java
package cn.kb.learn.functional;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

public class Pipeline {
    static List<String> process(List<String> source, List<Function<String, String>> steps) {
        List<String> result = new ArrayList<>();
        for (String item : source) {
            String current = item;
            for (Function<String, String> step : steps) {
                current = step.apply(current);
            }
            result.add(current);
        }
        return result;
    }

    static Function<String, String> truncate(int max) {
        return text -> text.length() <= max ? text : text.substring(0, max);
    }

    public static void main(String[] args) {
        List<String> source = List.of("  Hello World  ", " 知识库  测试 ", "Java");

        List<Function<String, String>> cleanPipeline = List.of(
                String::strip,
                String::toLowerCase);

        List<Function<String, String>> displayPipeline = List.of(
                String::strip,
                truncate(5),
                text -> "[" + text + "]");

        System.out.println("清洗管道: " + process(source, cleanPipeline));
        System.out.println("展示管道: " + process(source, displayPipeline));

        // 管道可以动态组合
        List<Function<String, String>> combined = new ArrayList<>(cleanPipeline);
        combined.addAll(displayPipeline);
        System.out.println("组合管道: " + process(source, combined));
    }
}

"把步骤当数据"的好处:

  1. 可组合:管道可以拼接(combined)、可以抽取成常量复用;
  2. 可配置:步骤列表可以来自配置或用户输入,不需要改代码;
  3. 易测试:每个步骤是独立的小函数,可以单独测试;整条管道也可以整体测试;
  4. 解耦:调用方只关心"有哪些步骤",不关心每步怎么实现。

这正是责任链模式的函数式实现,也是中间件、过滤器(Servlet Filter)、拦截器(Spring Interceptor)的设计思路。

预期输出:

text
清洗管道: [hello world, 知识库  测试, java]
展示管道: [[Hell], [知识库], [Java]]
组合管道: [[hello], [知识库], [java]]

13. 掌握标准 ​

  • [ ] 能定义并使用函数式接口;
  • [ ] 熟练使用四个内置函数式接口及其扩展;
  • [ ] 能写出四种方法引用,并"还原"成 Lambda 验证;
  • [ ] 知道变量捕获的限制与原因;
  • [ ] 能判断 Lambda 什么时候该提成方法(避免可读性下降)。

14. 扩展资源 ​

15. 下一步学习 ​

《Optional》:用函数式的方式优雅地处理"可能为空"的返回值。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引