Jakob Jenkov 2020-11-12
Java Lambda 表达式是 Java 8 中的新特性。Lambda 表达式是 Java 向函数式编程迈出的第一步。
一个 Java Lambda 表达式本质上是一个不属于任何类的函数,它可以像对象一样被传递,并在需要时执行。
Lambda 表达式常用于实现简单的事件监听器/回调函数,或与 Java Stream API 结合进行函数式编程。它也广泛应用于 Java 函数式编程 中。
Lambda 与单方法接口
函数式编程经常用于实现事件监听器。在 Java 中,事件监听器通常被定义为只包含一个方法的接口。例如,下面是一个虚构的单方法接口:
public interface StateChangeListener {
public void onStateChange(State oldState, State newState);
}
这个接口定义了一个方法,每当状态发生变化时(无论观察的是什么对象),该方法就会被调用。
在 Java 7 中,你需要实现这个接口才能监听状态变化。假设你有一个名为 StateOwner 的类,它可以注册状态事件监听器:
public class StateOwner {
public void addStateListener(StateChangeListener listener) {
// ...
}
}
在 Java 7 中,你可以使用匿名内部类来添加监听器:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// 对旧状态和新状态做些处理
}
});
首先创建一个 StateOwner 实例,然后为其添加一个匿名实现的 StateChangeListener 监听器。
而在 Java 8 中,你可以使用 Lambda 表达式来添加监听器:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
其中的 Lambda 表达式是这一部分:
(oldState, newState) -> System.out.println("State changed")
该 Lambda 表达式会与 addStateListener() 方法的参数类型进行匹配。如果 Lambda 表达式与参数类型(本例中是 StateChangeListener 接口)匹配,那么该表达式会被转换为一个实现了该接口的对象。
注意:Lambda 表达式只能用于匹配只有一个抽象方法的接口。上例中,Lambda 表达式作为参数传入,其类型是 StateChangeListener 接口,而该接口只有一个方法,因此匹配成功。
Lambda 与接口的匹配规则
只含一个方法的接口有时也被称为函数式接口(functional interface)。将 Lambda 表达式与函数式接口匹配的过程分为以下几步:
- 该接口是否只有一个抽象(未实现)方法?
- Lambda 表达式的参数是否与该单一方法的参数匹配?
- Lambda 表达式的返回类型是否与该单一方法的返回类型匹配?
如果以上三个问题的答案都是“是”,那么该 Lambda 表达式就能成功匹配该接口。
包含默认方法和静态方法的接口
从 Java 8 开始,Java 接口 可以包含默认方法(default methods)和静态方法(static methods)。这两种方法都在接口声明中直接提供了实现。
这意味着,只要接口中只有一个未实现的方法(即抽象方法),即使它包含多个默认或静态方法,仍然可以用 Lambda 表达式来实现。
换句话说,只要接口只有一个抽象方法,即使它有默认方法和静态方法,它仍然是一个函数式接口。
下面是一个可以使用 Lambda 表达式实现的接口示例:
import java.io.IOException;
import java.io.OutputStream;
public interface MyInterface {
void printIt(String text);
default public void printUtf8To(String text, OutputStream outputStream){
try {
outputStream.write(text.getBytes("UTF-8"));
} catch (IOException e) {
throw new RuntimeException("Error writing String as UTF-8 to OutputStream", e);
}
}
static void printItToSystemOut(String text){
System.out.println(text);
}
}
尽管该接口包含 3 个方法,但只有一个是未实现的(printIt),因此可以用 Lambda 表达式实现:
MyInterface myInterface = (String text) -> {
System.out.print(text);
};
Lambda 表达式 vs 匿名接口实现
虽然 Lambda 表达式与匿名接口实现非常相似,但它们之间有几个重要区别。
主要区别在于:匿名接口实现可以拥有状态(成员变量),而 Lambda 表达式不能。
考虑以下接口:
public interface MyEventConsumer {
public void consume(Object event);
}
你可以用匿名内部类实现它:
MyEventConsumer consumer = new MyEventConsumer() {
public void consume(Object event){
System.out.println(event.toString() + " consumed");
}
};
这个匿名实现可以拥有自己的内部状态。例如:
MyEventConsumer myEventConsumer = new MyEventConsumer() {
private int eventCount = 0;
public void consume(Object event) {
System.out.println(event.toString() + " consumed " + this.eventCount++ + " times.");
}
};
注意这里新增了一个字段 eventCount。
而 Lambda 表达式不能拥有这样的字段,因此 Lambda 被认为是无状态的(stateless)。
Lambda 类型推断(Type Inference)
在 Java 8 之前,使用匿名内部类时必须显式指定要实现的接口。例如:
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
// 处理状态变化
}
});
而使用 Lambda 表达式时,类型通常可以从上下文中推断出来。例如,编译器可以通过 addStateListener() 方法的参数类型(即 StateChangeListener 接口)推断出 Lambda 应该实现哪个接口。这种机制称为类型推断。
同样,Lambda 表达式中的参数类型也可以被推断。例如:
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("State changed")
);
这里没有显式写出 StateChangeListener 接口,也没有写出 oldState 和 newState 的类型,但编译器能从 onStateChange() 方法的签名中推断出它们的类型。
Lambda 参数
Lambda 表达式本质上是方法,因此可以像方法一样接收参数。例如前面的 (oldState, newState) 就是参数列表,必须与目标接口方法的参数匹配。
无参数
如果目标方法不接受任何参数,Lambda 写作:
() -> System.out.println("Zero parameter lambda");
括号内为空,表示无参数。
单个参数
如果只有一个参数,可以写作:
(param) -> System.out.println("One parameter: " + param);
或者省略括号:
param -> System.out.println("One parameter: " + param);
多个参数
多个参数必须用括号括起来:
(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);
只有单个参数时才可以省略括号。
参数类型
有时需要显式指定参数类型(当编译器无法推断时):
(Car car) -> System.out.println("The car is: " + car.getName());
这类似于普通方法中的参数声明。
Java 11 起支持 var 参数类型
从 Java 11 开始,Lambda 参数也可以使用 var 关键字(Java 10 引入了局部变量类型推断):
Function<String, String> toLowerCase = (var input) -> input.toLowerCase();
这里的 input 类型会被推断为 String,因为变量声明中泛型指定了 Function<String, String>。
Lambda 函数体
Lambda 的函数体写在 -> 的右侧。例如:
(oldState, newState) -> System.out.println("State changed")
如果函数体包含多行代码,需要用大括号 {} 包裹:
(oldState, newState) -> {
System.out.println("Old state: " + oldState);
System.out.println("New state: " + newState);
}
从 Lambda 表达式返回值
Lambda 表达式可以像普通方法一样返回值:
(param) -> {
System.out.println("param: " + param);
return "return value";
}
如果整个 Lambda 表达式只做一件事——计算并返回一个值,可以省略 return 和大括号:
// 冗长写法
(a1, a2) -> { return a1 > a2; }
// 简洁写法
(a1, a2) -> a1 > a2;
编译器会自动将表达式 a1 > a2 视为返回值。
Lambda 作为对象
Lambda 表达式本质上是一个对象,可以赋值给变量并传递:
public interface MyComparator {
public boolean compare(int a1, int a2);
}
MyComparator myComparator = (a1, a2) -> a1 > a2;
boolean result = myComparator.compare(2, 5);
变量捕获(Variable Capture)
Lambda 表达式可以在特定条件下访问其外部定义的变量,包括:
- 局部变量
- 实例变量
- 静态变量
局部变量捕获
Lambda 可以捕获外部的局部变量,但该变量必须是 “ effectively final ”(即赋值后不再改变):
String myString = "Test";
MyFactory myFactory = (chars) -> {
return myString + ":" + new String(chars);
};
如果 myString 后续被修改,编译器会报错。
实例变量捕获
Lambda 可以捕获创建它的对象中的实例变量,甚至可以在捕获后修改该变量:
public class EventConsumerImpl {
private String name = "MyConsumer";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(this.name); // 捕获实例变量
});
}
}
注意:在 Lambda 中,this 指向外围对象,而不是 Lambda 自身(因为 Lambda 没有自己的实例变量)。这与匿名内部类不同。
静态变量捕获
Lambda 也可以访问静态变量,且允许在捕获后修改:
public class EventConsumerImpl {
private static String someStaticVar = "Some text";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(someStaticVar);
});
}
}
方法引用(Method References)
当 Lambda 表达式只是简单地调用另一个方法时,可以使用方法引用来简化语法。
例如,有如下接口:
public interface MyPrinter{
public void print(String s);
}
普通 Lambda 写法:
MyPrinter myPrinter = s -> System.out.println(s);
使用方法引用:
MyPrinter myPrinter = System.out::println;
双冒号 :: 告诉编译器这是一个方法引用。
静态方法引用
public interface Finder {
public int find(String s1, String s2);
}
public class MyClass{
public static int doFind(String s1, String s2){
return s1.lastIndexOf(s2);
}
}
Finder finder = MyClass::doFind;
参数方法引用
引用第一个参数的方法:
Finder finder = String::indexOf;
// 等价于 (s1, s2) -> s1.indexOf(s2);
实例方法引用
引用某个对象的实例方法:
public interface Deserializer {
public int deserialize(String v1);
}
public class StringConverter {
public int convertToInt(String v1){
return Integer.valueOf(v1);
}
}
StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;
构造器引用
引用类的构造器:
public interface Factory {
public String create(char[] val);
}
Factory factory = String::new;
// 等价于 chars -> new String(chars);