Java lambda表达式是Java 8中的新增功能。Java lambda表达式是Java进入函数式编程的第一步。一个Java lambda表达式它不属于任何类,而可以看作是一个可以创建的函数。一个Java lambda表达式可以作为对象传递并按需执行。
Java lambda表达式通常用于实现简单的事件监听/回调,或在使用Java Streams API进行函数式编程时使用。
函数式编程通常用于实现事件监听器。Java中的事件监听器通常被定义为具有单个方法的Java接口。
例如以下示例:
public interface StateChangeListener {
public void onStateChange(State oldState, State newState);
}
这个Java接口定义了一个单独的方法,只要状态发生变化(无论观察到什么),都将调用该方法。
在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) {
//TODO
}
});
首先创建一个StateOwner实例。然后,将StateChangeListener接口的匿名实现作为监听器添加到StateOwner实例上。
而在Java 8中,你可以使用Java lambda表达式添加事件监听器,如下所示:
StateOwner stateOwner = new StateOwner();
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("状态变化了")
);
以下这部分就是lambda表达式的使用
(oldState, newState) -> System.out.println("状态变化了")
lambda表达式的参数与addStateListener() 方法的参数进行参数类型匹配。如果匹配,则将lambda表达式转换为实现与该参数相同的接口的方法。
Java lambda表达式只能在与它们匹配的类型是单个方法接口的地方使用。在上面的示例中,lambda表达式用作参数,其中参数类型为StateChangeListener接口。该接口只有一个方法。因此,lambda表达式已针对该接口成功匹配。
单个方法的接口有时也称为函数式接口。例如Runnable
接口
注:被FunctionalInterface
的注解修饰。
想要将Java lambda表达式与函数式接口进行匹配分为以下几个步骤:
如果这三个条件都符合的话,则将给定的lambda表达式则与接口成功匹配。
从Java 8开始,Java接口可以同时包含默认方法和静态方法。默认方法和静态方法都可以直接在接口中定义并实现。这意味着Java 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("OutputStream操作时发生了异常", e);
}
}
static void printItToSystemOut(String text){
System.out.println(text);
}
}
即使此接口包含3种方法,也可以通过lambda表达式实现,因为只有一种方法没有实现。
实现如下:
MyInterface myInterface = (String text) -> {
System.out.print(text);
};
即使lambda表达式接近匿名接口实现,也有一些区别是值得注意的。
主要区别在于,匿名接口实现可以具有状态(成员变量),而lambda表达式则不能。
看看如下示例:
public interface MyEventConsumer {
public void consume(Object event);
}
可以使用匿名接口实现来实现此接口:
MyEventConsumer consumer = new MyEventConsumer() {
public void consume(Object event){
System.out.println(event.toString() + " 消费了");
}
};
此匿名MyEventConsumer的实现可以具有自己的内部状态。改造下:
MyEventConsumer myEventConsumer = new MyEventConsumer() {
private int eventCount = 0;
public void consume(Object event) {
System.out.println(event.toString() + " 消费了 " + this.eventCount++ + " 次.");
}
};
请注意,MyEventConsumer的匿名函数实现中有一个名为eventCount的字段。而Lambda表达式中不能具有此类字段。所以lambda表达式被认为是无状态的。
在Java 8之前,在进行匿名接口实现时,你必须指定要实现的接口。
这是本文开头的匿名接口实现示例:
stateOwner.addStateListener(new StateChangeListener() {
public void onStateChange(State oldState, State newState) {
//TODO
}
});
使用lambda表达式时,通常可以从周围的代码中推断出类型。例如,可以从onStateChange()的方法声明中推断参数类型。这称为类型推断。
stateOwner.addStateListener(
(oldState, newState) -> System.out.println("状态变化了")
);
在lambda表达式中,通常也可以推断出参数类型。在上面的示例中,编译器可以从onStateChange()方法声明中推断其类型。因此,从onStateChange()方法的方法声明中推断出参数oldState和newState的类型。
由于Java lambda表达式实际上只是方法,因此lambda表达式可以像方法一样接受参数。前面显示的lambda表达式的(oldState,newState)部分指定lambda表达式采用的参数。这些参数必须与单个方法接口中的方法参数匹配。在这种情况下,这些参数必须与StateChangeListener接口的onStateChange()方法的参数匹配。
public void onStateChange(State oldState, State newState);
如果你要与lambda表达式进行匹配的方法不带参数,则可以这样编写lambda表达式:
() -> System.out.println("零参数 lambda");
请注意,括号之间没有内容。这表示Lambda不带任何参数。
如果你要匹配Java lambda表达式的方法采用一个参数,则可以这样编写lambda表达式:
(param) -> System.out.println("一个参数: " + param);
也可以省略括号,如下所示:
param -> System.out.println("一个参数: " + param);
如果你将Java lambda表达式与之匹配的方法带有多个参数,则这些参数需要在括号内列出。这样表示:
(p1, p2) -> System.out.println("多个参数: " + p1 + ", " + p2);
仅当方法采用单个参数时,才可以省略括号。
如果编译器无法从lambda匹配的功能接口方法推断参数类型,则有时可能需要为lambda表达式指定参数类型。不用担心,编译器会在这种情况下告诉你。这是一个lambda参数类型示例:
(Car car) -> System.out.println("这个汽车是: " + car.getName());
如你所见,car参数的类型(Car)写在参数名称的前面,就像你在其他方法中声明参数或对接口进行匿名实现时一样。
在Java 11中,你可以使用var关键字作为参数类型。 var关键字在Java 10中作为局部变量类型推断引入。从Java 11开始,var也可以用于lambda参数类型。这是在lambda表达式中使用Java var关键字作为参数类型的示例:
Function<String, String> toLowerCase = (var input) -> input.toLowerCase();
使用上面的var关键字声明的参数的类型将推断为String类型,因为变量的类型声明的通用类型设置为Function
lambda表达式的主体以及它表示的函数/方法的主体在lambda声明中的->
的右侧指定:
如下示例:
(oldState, newState) -> System.out.println("状态变化了")
如果你的lambda表达式需要包含多行,则可以将lambda函数主体放在{}
括号内,Java在其他地方声明方法时也需要使用该括号。如下例子:
(oldState, newState) -> {
System.out.println("旧的状态: " + oldState);
System.out.println("新的状态: " + newState);
}
你可以从Java lambda表达式返回值,就像从方法中返回值一样。您只需向lambda函数主体添加一个return语句,就像这样:
(param) -> {
System.out.println("参数: " + param);
return "返回值";
}
如果你的lambda表达式只需要计算一个返回值并将其返回,则可以用更短的方式指定返回值代替这个:
(a1, a2) -> { return a1 > a2; }
你可以写成:
(a1, a2) -> a1 > a2;
Java lambda表达式本质上是一个对象。你可以将lambda表达式分配给变量并将其传递,就像处理任何其他对象一样。如下例子:
public interface MyComparator {
public boolean compare(int a1, int a2);
}
MyComparator myComparator = (a1, a2) -> a1 > a2;
boolean result = myComparator.compare(2, 5);
第一个代码块显示了lambda表达式实现的接口。第二个代码块显示了lambda表达式的定义,lambda表达式如何分配给变量,以及最后如何通过调用其实现的接口方法来调用lambda表达式。
Java lambda表达式在某些情况下能够访问在lambda函数主体外部声明的变量。
Java lambda可以捕获以下类型的变量:
Java lambda可以捕获在lambda主体外部声明的局部变量的值。为了说明这一点,请看以下单个方法的接口:
public interface MyFactory {
public String create(char[] chars);
}
现在,看一下实现MyFactory接口的lambda表达式:
MyFactory myFactory = (chars) -> {
return new String(chars);
};
现在,此lambda表达式仅引用传递给它的chars参数值。但我们可以做下改变,这是引用在lambda函数body外部声明的String变量的修改版本:
String myString = "测试";
MyFactory myFactory = (chars) -> {
return myString + ":" + new String(chars);
};
如你所见,lambda body现在引用了在lambda body外部声明的局部变量myString。当且仅当被引用的变量是“有效的final变量”时才有可能,这意味着在赋值之后它不会更改其值。如果myString变量的值后来发生了变化,编译器会抱怨在lambda body内部引用它。
Lambda表达式还可以捕获创建Lambda的对象中的实例变量。以下是举例说明:
public class EventConsumerImpl {
private String name = "我的消费者";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(this.name);
});
}
}
注意lambda body中对this.name
的引用。这将捕获封闭的EventConsumerImpl对象的name
实例变量。
甚至有可能在捕获实例变量后更改其值-该值将反映在lambda内部。
这种语义实际上是Java lambda与接口的匿名实现不同的领域之一。匿名接口实现可以具有自己的实例变量,可以通过this
引用进行引用。但是,lambda不能拥有自己的实例变量,因此它始终指向封闭的对象。
(注意:上述的消费者事件设计不是特别优雅。我只是这样来说明实例变量捕获。)
Java lambda表达式还可以捕获静态变量。这并不奇怪,因为只要可访问静态变量(包作用域或public公共的),Java应用程序中的任何地方都可以访问静态变量。
这是一个示例类,该类创建一个lambda,该lambda从lambda主体内部引用静态变量:
public class EventConsumerImpl {
private static String someStaticVar = "文本内容";
public void attach(MyEventProducer eventProducer){
eventProducer.listen(e -> {
System.out.println(someStaticVar);
});
}
}
lambda捕获到静态变量后,它的值也可以更改。
同样,上述类设计不是很好,该类主要用于向你显示lambda可以访问静态变量。
如果你的lambda表达式所做的只是调用另一个方法并传递lambda的参数给它,其实Java的lambda实现则提供了一种表达Method
调用的简短方法。
首先,看下面这个单个函数接口的示例:
public interface MyPrinter{
public void print(String s);
}
以下是创建实现MyPrinter接口的Java lambda实例的示例:
MyPrinter myPrinter = (s) -> { System.out.println(s); };
由于lambda body仅包含一个语句,因此我们实际上可以省略括号{}
。
另外,由于lambda方法只有一个参数,因此我们可以省略该参数周围的括号()
。
例如:
MyPrinter myPrinter = s -> System.out.println(s);
由于所有lambda body所做的工作都是将字符串参数传递给System.out.println()方法,因此我们可以将上述lambda声明替换为方法引用:
MyPrinter myPrinter = System.out::println;
注意双冒号::
。这是向Java编译器发出信号,表明这是方法引用。
引用的方法是双冒号之后的内容。拥有引用方法的任何类或对象都在双冒号之前。
您可以引用以下类型的方法:
接下来介绍每种类型的方法引用。
最容易引用的方法是静态方法。这是第一个单函数接口的示例:
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);
}
}
最后是引用静态方法的Java lambda表达式:
Finder finder = MyClass::doFind;
由于Finder.find() 和MyClass.doFind()方法的参数匹配,因此可以创建实现Finder.find()并引用MyClass.doFind()方法的lambda表达式。
你还可以将方法中的某一个参数引用到lambda。看如下单一函数接口:
public interface Finder {
public int find(String s1, String s2);
}
该接口旨在表示能够在s1中搜索是否出现s2的内容。
以下是一个Java lambda表达式的示例,该表达式调用String.indexOf()进行搜索
Finder finder = String::indexOf;
这等价于如下lambda定义:
Finder finder = (s1, s2) -> s1.indexOf(s2);
请注意便捷版的是如何引用单一函数的。Java编译器将尝试使用第二参数类型作为引用方法的参数,将引用方法与第一参数类型进行匹配。
第三,还可以从lambda定义中引用实例方法。
首先,让我们看一个方法接口定义:
public interface Deserializer {
public int deserialize(String v1);
}
此接口表示一个组件,该组件能够将字符串“反序列化”为int。
现在看一下这个StringConverter类:
public class StringConverter {
public int convertToInt(String v1){
return Integer.valueOf(v1);
}
}
convertToInt()方法与Deserializer类的deserialize()方法具有相同的签名。
因此,我们可以创建StringConverter的实例,并从Java lambda表达式引用其convertToInt()方法,如下所示:
StringConverter stringConverter = new StringConverter();
Deserializer des = stringConverter::convertToInt;
第二行创建的lambda表达式引用第一行中StringConverter实例的convertToInt方法。
最后,对于类的构造函数也可以进行引用。你可以通过在类名后面加上:: new
来完成此操作,如下所示:
MyClass::new
要了解如何将构造函数用作lambda表达式,请查看以下接口定义:
public interface Factory {
public String create(char[] val);
}
此接口的create()方法与String类中构造函数之一的签名匹配。因此,此构造函数可用作lambda。
看如下例子:
Factory factory = String::new;
这等价于以下Java lambda表达式:
Factory factory = chars -> new String(chars);