主题
界面
主题
Lambda 让"函数"可以像值一样传递:list.removeIf(name -> name.length() > 5) 里的 name -> name.length() > 5 就是一个函数。
它改变了 Java 的写法——《Stream API》 的每个操作都建立在 Lambda 之上,《Optional》 与并发工具同理。本节把 Lambda 的语法、方法引用与变量捕获讲透。
完成本节后,你应该能够:
Function、Consumer、Supplier、Predicate 四个内置接口表达常见意图;this 语义上的区别。List.sort 或 Comparator 吗?(《HashSet、TreeSet 与集合排序》)看一个"按字符串长度排序"的需求,三种写法:
// 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、并发工具能设计成现在这样的前提。
函数式接口 = 只有一个抽象方法的接口(可以有默认方法与静态方法)。
@FunctionalInterface // 可选,但能帮编译器检查
interface Calculator {
long calculate(long a, long b);
}
Calculator add = (a, b) -> a + b; // Lambda 就是它的实例@FunctionalInterface 的作用:如果接口有零个或两个以上抽象方法,编译直接失败。它是一道防错栏。
为什么必须依附接口:Java 没有独立的"函数类型",Lambda 必须对应某个接口类型。编译器根据上下文推断是哪个接口——这叫"目标类型"。
// 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() |
| 没有参数 | 写空括号:() -> "值" |
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)。
// 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 看是否说得通。
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 形式是"参数作为调用者"。
int base = 100;
Calculator addBase = (a, b) -> a + b + base; // 可以
base = 200; // 编译错误:base 必须是 final 或等效 final规则:Lambda 可以读取外层的局部变量,但这些变量必须是 final 或"事实上没有再次赋值"(effectively final)。
为什么限制这么严:Lambda 可能在另一个线程、另一个时刻执行,如果允许修改局部变量,就会出现"变量存在于栈上但函数已经跑到了别处"的矛盾。Java 选择直接禁止,而不是引入闭包捕获的复杂性。
类字段不受此限制:
private int counter; // 字段
Runnable task = () -> counter++; // 可以(操作的是 this.counter)| Lambda | 匿名内部类 | |
|---|---|---|
this 的含义 | 外层实例的 this | 自身实例的 this |
| 能否访问外层局部变量 | 只能读 effect-final | 同样只能读 |
| 代码量 | 少 | 多 |
| 适用 | 函数式接口 | 需要抽象类或非函数式接口时 |
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 时才用匿名内部类。
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同一个方法,传不同的行为——这是"把函数作为参数"的核心价值。
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()); // 排序三行代码完成过去需要循环 + 判断 + 临时集合的工作,且意图更明确:每一行都在说"做什么",而不是"怎么做"。
// 难看:嵌套太深
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 提成方法,用方法引用:
list.stream()
.filter(this::isPaidOrder)
.map(this::calculateTotal)
.forEach(System.out::println);判断标准:Lambda 体超过 3 行,或需要注释才能看懂,就该提成方法。 方法名本身是最好的注释。
文件:labs/functional/src/main/java/cn/kb/learn/functional/LambdaLesson.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);
}
}运行命令:
cd labs
./mvnw -pl functional compile
java -cp functional/target/classes cn.kb.learn.functional.LambdaLesson预期输出:
匿名内部类: 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 的日常用法。
apply(add, 10, 3) 处断点并按 F7 进入,观察 Lambda 内部如何执行——调试器里它会显示成一个类似 LambdaLesson.lambda$main$0 的方法名,说明 Lambda 编译后仍然是方法;lambda instanceof Calculator 处断点,展开 lambda 对象,看到它的类型是 LambdaLesson$$Lambda$...——编译器生成的实现类;names.removeIf(name -> name.length() > 5) 处 F7 进入 ArrayList.removeIf,观察它内部用 Predicate.test 遍历判断;int base = 100; 后面加上 base = 200;,观察编译错误("从 lambda 表达式引用的本地变量必须是 final 或实际上的 final")。int count = 0;
list.forEach(item -> count++); // 编译错误:count 不是 effectively final修复:改用可变容器(但不推荐)、或改用流式归约:
long count = list.stream().filter(...).count();不要为了绕过限制而写 int[] count = {0}; list.forEach(x -> count[0]++);——这是反模式,掩盖了"应该用流式操作"的事实。
Calculator multiply = (a, b) -> {
a * b; // 编译错误:缺少 return
};this 不是内部类实例 class Demo {
String name = "外层";
Supplier<String> getter() {
return () -> this.name; // this 是 Demo 实例,不是 Supplier 实例
}
}这与匿名内部类的行为不同(5.6 节)。
BiFunction<String, String, Boolean> startsWith = String::startsWith;
// 等价于 (a, b) -> a.startsWith(b),不是 b.startsWith(a)修复:写方法引用前先"还原"成 Lambda 确认。
见 6.3 节。记住:Lambda 的目标是表达力,不是字符数。
目标:用 Lambda 重构一段命令式代码。
步骤:
List<String> filterAndTransform(List<String> source, Predicate<String> filter, Function<String, String> mapper):先过滤再转换(不修改入参);["apple", "fig", "banana", "kiwi"] 中长度大于 3 的转成大写并返回;Consumer 写一个方法 void forEachWithIndex(List<String> list, BiConsumer<Integer, String> action):带下标遍历;验收方式:能说明"什么时候用 Lambda、什么时候用方法引用"(提示:方法引用更简洁但可读性依赖方法名)。
扩展任务:自定义一个函数式接口 Validator<T>(List<String> validate(T target)),用它表达"校验规则",并写一个 validateAll(T target, List<Validator<T>> validators) 把多个规则组合起来——体会"把行为当数据"的设计方式。
基础题
Function 的抽象方法是 ______。代码阅读题
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"));f1 与 f2 有区别吗?编程题
long reduce(List<Integer> numbers, BinaryOperator<Integer> operator, int identity): identity 作为初始值,依次把每个元素与累积值交给 operator;[1,2,3,4] 分别求"求和"与"求最大值";identity 在求最大值时应该取什么值。综合任务
List<String> process(List<String> source, List<Function<String, String>> steps):把多个转换步骤依次应用到每个元素;提示 1(方向):第 4 题的两个方法引用与 Lambda 完全等价,输出相同。
提示 2(关键点):第 5 题求最大值的 identity 应该取 Integer.MIN_VALUE(或列表最小值),因为累积的起点不能比任何元素大。
提示 3(接近答案):第 6 题的 process 对每个元素依次应用 steps 里的函数,可以用循环也可以再叠一层 reduce。
R apply(T t)。final 或 effectively final,以避免这种复杂性。[A, CCC]
4 / 4removeIf 去掉了 "bb",剩下 ["a", "ccc"];replaceAll(String::toUpperCase) 转成大写得 [A, CCC];f1.apply("test") 与 f2.apply("test") 结果相同(都是 4)。f1 是方法引用、f2 是等价 Lambda,编译后的行为完全一致。方法引用只是更简洁的写法,不涉及性能差异。
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——这是"约简初始值"最容易出错的地方。
预期输出:
求和: 10
求最大值: 4package 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));
}
}"把步骤当数据"的好处:
combined)、可以抽取成常量复用;这正是责任链模式的函数式实现,也是中间件、过滤器(Servlet Filter)、拦截器(Spring Interceptor)的设计思路。
预期输出:
清洗管道: [hello world, 知识库 测试, java]
展示管道: [[Hell], [知识库], [Java]]
组合管道: [[hello], [知识库], [java]]《Optional》:用函数式的方式优雅地处理"可能为空"的返回值。