可以四处传递的行为
lambda 表达式是一小段代码,你可以把它交给方法、存进变量或者作为返回值,就像处理数字或字符串一样。在有 lambda 之前,传递行为意味着要写一个完整的类(或者一个冗长的匿名类),只为了包住一个方法。lambda 把这些精简到了本质:参数和方法体。
lambda 总是实现一个函数式接口,也就是只有一个抽象方法的接口(你在接口那一篇的结尾见过它们)。编译器会根据上下文判断你指的是哪个接口,并把 lambda 对应到那唯一的方法上。
x -> x * 2 就是 apply 的全部实现。没有 new,没有类的主体,也没有方法名,这些都由接口提供。
箭头语法
每个 lambda 的形式都是 parameters -> body。各部分可以根据需要灵活变化:
() -> 42 // no parameters
x -> x + 1 // one parameter, parentheses optional
(x, y) -> x + y // two or more parameters need parentheses
(int x, int y) -> x + y // types are optional - usually inferred
x -> { // a block body needs braces and return
int doubled = x * 2;
return doubled + 1;
}
单个表达式作为方法体(x -> x + 1)时,会隐式返回它的值,不需要 return 关键字。一旦用了花括号,你写的就是一个普通代码块,如果方法有返回值,就必须显式地 return。常见的错误是把两者混在一起:x -> { x + 1 } 无法编译,因为代码块需要的是一条语句(return x + 1;)。
lambda 取代匿名类
要看清 lambda 带来了什么,最直观的方法是做个前后对比。以前用自定义的 Comparator 排序是这样写的:
// before - anonymous class
names.sort(new Comparator<String>() {
public int compare(String a, String b) {
return a.length() - b.length();
}
});
同样的功能用 lambda 写,只需易读的一行:
Comparator 是一个函数式接口(它唯一的抽象方法是 compare),所以 lambda 可以直接放进去。所有的繁文缛节,包括 new、类和方法签名,全都不见了,只剩下比较逻辑。
java.util.function 工具箱
你很少需要自己声明函数式接口。java.util.function 包提供了常见的几种形式,几乎所有的 Java 库 API 都接受它们:
Function<T, R>:接收一个T,返回一个R(apply)Predicate<T>:接收一个T,返回一个boolean(test)Supplier<T>:不接收参数,返回一个T(get)Consumer<T>:接收一个T,不返回任何东西(accept)
这些都是泛型接口:Function<String, Integer> 复用了你在上一篇看到的泛型来保证类型安全。根据你的代码需要接收什么、产出什么,选择形式与之匹配的那一个。
方法引用
当一个 lambda 只是调用一个现有的方法时,你可以用 :: 把它换成方法引用。它们的值是一样的,只是写法更直接:
方法引用有几种形式:String::toUpperCase(对每个参数调用的实例方法)、Math::max(静态方法)、System.out::println(某个特定对象上的方法),以及 ArrayList::new(构造函数)。只在读起来清晰时才用它;如果你还得想一想该用哪种形式,那直接写 lambda 也完全没问题。
捕获变量
lambda 可以使用外层方法中的局部变量,但前提是它们是 final 或事实上的 final,也就是只赋值一次、之后再也不改变。Java 在创建 lambda 的那一刻捕获变量的值,所以一个之后可能改变的变量会造成歧义。
如果你在任何地方给 factor 重新赋值,编译器就会拒绝这个 lambda,并报错“variable used in lambda expression should be final or effectively final”。如果你确实需要可变的共享状态,就改为捕获一个对象:一个字段、一个数组元素或一个 AtomicInteger,因为即使内容改变,引用本身是固定的。注意,lambda 和匿名类不同,它不会创建自己的作用域:lambda 中的 this 指向外层的实例,而不是 lambda 本身。
下一篇:Stream
lambda 是基石,而不是终点。它真正的价值体现在 Stream API 中:你可以把 filter、map、reduce 等操作串起来,每个操作都接收一个 lambda,从而把数据转换写成一条易读的流水线,而不是一团纠缠的循环。这就是下一篇的内容。
常见问题
Java 中的 lambda 表达式是什么?
lambda 是编写函数式接口(只有一个抽象方法的接口)实例的简短方式。你不用写一整个匿名类,只需写 parameters -> body。编译器会把 lambda 和接口的那一个方法对应起来,所以 Runnable r = () -> System.out.println("hi"); 就是一个完整的 Runnable。lambda 让你可以像传递数据一样传递行为。
Java 中 lambda 和方法引用有什么区别?
两者都会产生一个函数式接口的实例。lambda 有显式的参数和方法体(s -> s.toUpperCase()),而方法引用是一种简写,代表只调用一个现有方法的 lambda(String::toUpperCase)。当 lambda 只是把参数转交给某个具名方法时,就用方法引用,它更短,也更好读。
为什么 Java lambda 中使用的变量必须是 final 或事实上的 final?
lambda 可以捕获外层方法中的局部变量,但前提是这些变量赋值后再也不会改变,这就是“事实上的 final”(effectively final)的含义。Java 捕获的是值,而不是指向变量的实时引用,所以允许重新赋值会造成歧义,在多线程下也不安全。如果需要可变的共享状态,请改用字段,或者用数组、AtomicInteger 包装一下。