本节目标:能把一个「只有一段行为不同」的重复代码改写成 Lambda,理解函数式接口、目标类型、方法引用和变量捕获的规则,并能在
library里用Predicate、BookFormatter、Supplier表达搜索、格式化和延迟创建。
适用版本:JDK 25 LTS(Temurin 25.0.5);Lambda 与方法引用语法在 JDK 21 上没有差异。
上一节留下的问题
第 11 章末尾,LibraryService.search 已经接收一个 Predicate<Book>:
public List<Book> search(Predicate<Book> criteria) {
return books.findAll().stream().filter(criteria).toList();
}
但如果没有 Lambda,调用方只能写匿名内部类:
List<Book> recent = service.search(new Predicate<Book>() {
@Override
public boolean test(Book book) {
return book.year() > 2015;
}
});
五行代码只表达了一个条件:book.year() > 2015。如果还要按作者、按标题、按年份区间搜索,每个条件都套一层同样的壳。排序也一样:
books.sort(new Comparator<Book>() {
@Override
public int compare(Book left, Book right) {
return left.title().compareTo(right.title());
}
});
问题不在于匿名类做不到,而在于它把「真正想传的行为」埋在了样板代码里。Lambda 要解决的就是这件事:把一段行为当成参数传进去,语法上只保留这段行为本身。
函数式接口:只有一个抽象方法
Lambda 不能凭空存在,它必须对应一个函数式接口(functional interface):有且只有一个抽象方法的接口。Predicate<T> 就是这样一个接口:抽象方法 test(T) 负责判断,and、or、negate 是默认方法,isEqual 是静态工厂。
default 方法和 static 方法不算抽象方法,所以 Predicate 仍然只有一个抽象方法 test。@FunctionalInterface 是给编译器的声明:如果接口里出现第二个抽象方法,直接报错。
@FunctionalInterface
interface TwoMethods {
void first();
void second();
}
错误: 意外的 @FunctionalInterface 批注
@FunctionalInterface
^
TwoMethods 不是函数接口
在 接口 TwoMethods 中找到多个非覆盖抽象方法
JDK 自带一组常用的函数式接口,都在 java.util.function 里:
| 接口 | 抽象方法 | 用途 |
|---|---|---|
Predicate<T> | boolean test(T) | 条件判断 |
Function<T, R> | R apply(T) | 类型转换 |
Consumer<T> | void accept(T) | 消费一个值 |
Supplier<T> | T get() | 延迟提供一个值 |
UnaryOperator<T> | T apply(T) | 同类型转换 |
BiFunction<T, U, R> | R apply(T, U) | 两个输入一个输出 |
library 里也可以定义自己的函数式接口。比如「把一本书格式化成一行文本」:
@FunctionalInterface
public interface BookFormatter {
String format(Book book);
default BookFormatter withPrefix(String prefix) {
return book -> prefix + format(book);
}
}
withPrefix 返回一个新的 BookFormatter,内部调用当前实例的 format。默认方法让函数式接口可以组合,而不破坏「只有一个抽象方法」的约束。
Lambda 的语法与目标类型
Lambda 的基本形状是「参数列表 -> 方法体」:
Predicate<Book> recent = book -> book.year() > 2015;
Function<Book, String> titleOf = book -> book.title();
Supplier<Book> factory = () -> new Book(
"978-7-111-54742-6", "Effective Java", "Joshua Bloch", 2018);
Consumer<Book> printer = book -> System.out.println(book.title());
规则很简单:
- 参数只有一个时可以省略括号,如
book -> ...;没有参数时写()。 - 方法体是单个表达式时可以不写
return和花括号。 - 方法体有多条语句时用
{ ... },有返回值就要写return。 - 参数类型可以省略,编译器从目标类型推断;也可以显式写
(Book book) -> ...。
「目标类型」是关键。同一个 Lambda 可以赋给不同的函数式接口,只要参数和返回值匹配:
Predicate<String> isEmpty = value -> value.isEmpty();
Function<String, Boolean> same = value -> value.isEmpty();
两行代码的 Lambda 体一模一样,但目标类型不同,编译器生成的接口实现也不同。反过来,没有目标类型就写不出来:
var unknown = value -> value.isEmpty(); // 编译错误:需要目标类型
var 在这里帮不上忙,因为编译器不知道 value 是什么类型,也不知道要实现哪个接口。
方法引用:Lambda 的简写
当 Lambda 只是「直接调用一个已有方法」时,可以用方法引用(method reference)进一步缩短。常见的有四种:
Function<String, Integer> parse = Integer::parseInt; // 静态方法
Function<Book, String> title = Book::title; // 实例方法,未绑定
Consumer<String> println = System.out::println; // 实例方法,已绑定
Supplier<Book> factory = () -> new Book("978-7-111-54742-6",
"Effective Java", "Joshua Bloch", 2018); // 构造器可用 Book::new
Book::title 看起来像静态方法,实际是「未绑定的实例方法引用」:调用时第一个参数当作接收者,所以它匹配 Function<Book, String>。System.out::println 则是「已绑定的实例方法引用」,接收者 System.out 已经确定,剩下的参数才由函数式接口提供。
方法引用的可读性通常更好,但不要硬套。带条件的逻辑、需要多个语句的逻辑,还是用 Lambda 更清楚。
变量捕获与 this
Lambda 可以读取外层局部变量,但这个变量必须是事实上的 final(effectively final):声明后没有再被赋值。
int count = 0;
Runnable task = () -> System.out.println(count++);
错误: 从lambda 表达式引用的本地变量必须是最终变量或实际上的最终变量
Runnable task = () -> System.out.println(count++);
^
原因在于 Lambda 捕获的是变量的值,而不是变量本身。如果允许修改,多个线程或多次调用之间就无法保证看到同一个值。需要可变计数时,用 AtomicInteger 或数组这类容器,而不是修改局部变量。
另一个容易忽略的差异是 this。匿名内部类有自己的 this,Lambda 没有:
class Shadow {
String value = "outer";
Runnable anonymous() {
return new Runnable() {
String value = "anonymous";
@Override
public void run() {
System.out.println("anonymous -> " + value);
}
};
}
Runnable lambda() {
return () -> System.out.println("lambda -> " + value);
}
}
本机 JDK 25 输出:
anonymous -> anonymous
lambda -> outer
匿名类里的 value 解析到匿名类自己的字段;Lambda 里的 value 解析到外层 Shadow 的字段,因为 Lambda 不引入新的作用域。这个差异在写回调时很重要:Lambda 里的 this 就是创建它的那个对象。
在 library 里落地
搜索是最直接的应用。把条件写成 Predicate<Book>,服务层只负责过滤:
public List<Book> search(Predicate<Book> criteria) {
return books.findAll().stream().filter(criteria).toList();
}
调用方可以组合条件:
Predicate<Book> byBloch = book -> book.author().equals("Joshua Bloch");
Predicate<Book> after2010 = book -> book.year() > 2010;
List<Book> result = service.search(byBloch.and(after2010));
and、or、negate 都是 Predicate 的默认方法,组合出来的仍然是 Predicate<Book>。如果某个条件只是调用一个现有方法,方法引用会更短:
List<Book> titled = service.search(book -> book.title().startsWith("Java"));
格式化则用自定义的 BookFormatter:
BookFormatter formatter = book -> book.title() + " / " + book.author();
System.out.println(formatter.withPrefix("[书] ").format(DemoData.BOOKS.get(0)));
输出:
[书] Effective Java / Joshua Bloch
Supplier<LibraryException> 用来延迟创建异常。只有真正要抛的时候才构造对象,异常消息里的参数也能在那一刻求值:
Supplier<LibraryException> missing =
() -> new BookNotFoundException("978-7-000-00000-0");
System.out.println(missing.get().getMessage());
输出:
找不到 ISBN 为 978-7-000-00000-0 的图书
完整跑一遍:
LibraryService service = DemoData.service();
Predicate<Book> byBloch = book -> book.author().equals("Joshua Bloch");
List<Book> blochBooks = service.search(byBloch);
System.out.println("count = " + blochBooks.size());
Predicate<Book> after2010 = book -> book.year() > 2010;
System.out.println("and = " + service.search(byBloch.and(after2010)).stream()
.map(Book::title).toList());
BookFormatter formatter = book -> book.title() + " / " + book.author();
System.out.println(formatter.withPrefix("[书] ").format(DemoData.BOOKS.get(0)));
Function<Book, String> titleOf = Book::title;
System.out.println("mapped = " + titleOf.apply(DemoData.BOOKS.get(0)));
本机 JDK 25 输出:
count = 2
and = [Effective Java]
[书] Effective Java / Joshua Bloch
mapped = Effective Java
count = 2 是因为示例数据里 Joshua Bloch 有两本书;and 又把 2005 年的《Java Puzzlers》过滤掉,只剩 2018 年的《Effective Java》。
常见错误与边界
错误一:修改被捕获的局部变量
前面已经看到,count++ 会直接编译失败。即使变量在 Lambda 创建后不再修改,只要编译器无法证明它事实上 final,也会拒绝。正确做法是把可变状态放进对象字段,或用 AtomicInteger:
AtomicInteger calls = new AtomicInteger();
Runnable task = () -> calls.incrementAndGet();
task.run();
System.out.println(calls.get());
输出 1。容器本身没有变,变的是容器里的值。
错误二:把 Lambda 用在非函数式接口上
Object value = book -> book.title();
Object 不是函数式接口,Lambda 没有目标类型,编译失败。必须显式指定一个函数式接口:
Function<Book, String> title = book -> book.title();
Object value = title;
第二行把函数对象赋给 Object 是合法的,因为此时 Lambda 已经通过 Function<Book, String> 完成了类型检查。
错误三:重载让 Lambda 产生歧义
@FunctionalInterface
interface Action {
void run();
}
class OverloadDemo {
static void run(Runnable task) { }
static void run(Action task) { }
void demo() {
run(() -> System.out.println("done"));
}
}
错误: 对run的引用不明确
run(() -> System.out.println("done"));
^
OverloadDemo 中的方法 run(Runnable) 和 OverloadDemo 中的方法 run(Action) 都匹配
Runnable 和 Action 的抽象方法签名都是 void run(),Lambda 同时满足两者,编译器无法判断该选哪一个。注意如果其中一个接口的方法有返回值(比如 Callable<String>),() -> "done" 只满足有返回值的那一个,就不会歧义——歧义发生在两个 Lambda 形状相同的重载之间。解决办法是显式写类型转换 run((Runnable) () -> System.out.println("done")),或者改方法名。日常 API 设计里,尽量避免让两个重载只靠函数式接口区分。
边界:受检异常与函数式接口
Runnable.run() 不能抛受检异常,Callable.call() 可以。把一段会抛 IOException 的逻辑塞进 Runnable,就必须在 Lambda 体里自己 try/catch。第 13 章会系统讲受检异常;现在只需记住:函数式接口的方法签名决定了 Lambda 能不能抛异常。
小结
- 函数式接口是只有一个抽象方法的接口,
default和static方法不计数;@FunctionalInterface让编译器替你守住这条规则。 - Lambda 的写法是「参数 -> 方法体」,参数类型通常省略,由目标类型推断;没有目标类型就写不出来。
- 方法引用是「只调用一个现有方法」时的简写,分静态、未绑定实例、已绑定实例和构造器四类。
- Lambda 捕获的局部变量必须事实 final;Lambda 不引入新作用域,
this指向创建它的对象。 Predicate、Function、Consumer、Supplier覆盖了判断、转换、消费、提供四类常见行为,library的搜索和格式化可以直接复用。- 受检异常能否抛出由函数式接口的方法签名决定,
Runnable和Callable的差异就是例子。
Lambda 让「传一段行为」变短了,但如果要对集合做多次过滤、映射、排序、统计,逐个写 Lambda 仍然会堆成嵌套循环。下一节的 Stream 管道把这些步骤串成一条可读的流水线。
阅读导航:上一节:11.3 类型擦除与运行时真相 · 下一节:12.2 Stream 管道 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。