Java Lambda表达式
Java lambda表达式是Java 8中的新增函数。Javalambda表达式是Java进入函数式编程的第一步。因此,Java lambda表达式是可以创建的函数,而无需属于任何类。可以将Java lambda表达式作为对象传递并按需执行。
Java lambda表达式通常用于实现简单的事件侦听器/回调,或者在使用Java Streams API进行函数编程时使用。
如果我们喜欢视频,可以在Java Lambda Expression YouTube播放列表中找到本教程的视频版本。这是此播放列表中的第一个视频:
Java Lambdas和单一方法接口
函数式编程通常用于实现事件侦听器。 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) { // do something with the old and new state. } });
首先创建一个" StateOwner"实例。然后,将StateChangeListener接口的匿名实现添加为StateOwner实例上的侦听器。
在Java 8中,我们可以使用Java 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表达式转换为实现与该参数相同的接口的函数。
Java lambda表达式只能在与它们匹配的类型是单个方法接口的地方使用。在上面的示例中,lambda表达式用作参数,其中参数类型为" StateChangeListener"接口。该接口只有一个方法。因此,lambda表达式已针对该接口成功匹配。
将Lambda匹配到接口
单个方法接口有时也称为函数接口。将Java lambda表达式与函数接口进行匹配分为以下步骤:
- 接口是否只有一种抽象(未实现)方法?
- lambda表达式的参数是否与单个方法的参数匹配?
- lambda表达式的返回类型是否与单个方法的返回类型匹配?
如果对这三个问题的回答为"是",则将给定的lambda表达式与该接口成功匹配。
具有默认方法和静态方法的接口
从Java 8开始,Java接口可以同时包含默认方法和静态方法。默认方法和静态方法都具有直接在接口声明中定义的实现。这意味着,Java lambda表达式可以使用多种方法来实现接口,只要该接口仅具有一个未实现的(AKA抽象)方法即可。
换句话说,即使该接口包含默认方法和静态方法,只要该接口仅包含单个未实现(抽象)的方法,它仍然是函数性接口。这是此小节的视频版本:
可以使用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种方法,也可以通过lambda表达式实现,因为只有一种方法没有实现。这是实现的外观:
MyInterface myInterface = (String text) -> { System.out.print(text); };
Lambda表达式与匿名接口实现
即使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 myEventConsumer = new MyEventConsumer() { private int eventCount = 0; public void consume(Object event) { System.out.println(event.toString() + " consumed " + this.eventCount++ + " times."); } };
请注意,匿名MyEventConsumer实现现在如何具有一个名为eventCount的字段。
Lambda表达式不能包含此类字段。因此,lambda表达式被认为是无状态的。
Lambda类型推断
在Java 8之前,在进行匿名接口实现时,必须指定要实现的接口。这是本文开头的匿名接口实现示例:
stateOwner.addStateListener(new StateChangeListener() { public void onStateChange(State oldState, State newState) { // do something with the old and new state. } });
使用lambda表达式时,通常可以从周围的代码中推断出类型。例如,可以从addStateListener()方法的方法声明(StateChangeListener接口上的单个方法)推断出参数的接口类型。这称为类型推断。编译器通过在其他地方寻找类型(在这种情况下为方法定义)来推断参数的类型。这是本文开头的示例,显示了lambda表达式中未提及" StateChangeListener"接口:
stateOwner.addStateListener( (oldState, newState) -> System.out.println("State changed") );
在lambda表达式中,通常也可以推断出参数类型。在上面的示例中,编译器可以从onStateChange()方法声明中推断出其类型。因此,从onStateChange()方法的方法声明中推断出参数oldState和newState的类型。
Lambda参数
由于Java lambda表达式实际上只是方法,因此lambda表达式可以像方法一样接受参数。前面显示的lambda表达式的(oldState,newState)
部分指定lambda表达式采用的参数。这些参数必须与单个方法界面上的方法参数匹配。在这种情况下,这些参数必须与StateChangeListener接口的onStateChange()方法的参数匹配:
public void onStateChange(State oldState, State newState);
lambda表达式中的参数数量必须至少与方法匹配。
其次,如果我们在lambda表达式中指定了任何参数类型,则这些类型也必须匹配。我还没有向我们展示如何在lambda表达式参数上放置类型(本文稍后显示),但是在许多情况下,我们不需要它们。
零参数
如果我们要与lambda表达式进行匹配的方法不带参数,则可以这样编写lambda表达式:
() -> System.out.println("Zero parameter lambda");
请注意,括号之间没有内容。那是为了表示lambda不带任何参数。
一个参数
如果我们要匹配Java lambda表达式的方法采用一个参数,则可以这样编写lambda表达式:
(param) -> System.out.println("One parameter: " + param);
请注意,该参数在括号内列出。
当lambda表达式采用单个参数时,我们也可以省略括号,如下所示:
param -> System.out.println("One parameter: " + param);
多个参数
如果我们将Java lambda表达式与之匹配的方法带有多个参数,则需要在括号内列出这些参数。这是Java代码中的样子:
(p1, p2) -> System.out.println("Multiple parameters: " + p1 + ", " + p2);
仅当方法采用单个参数时,才可以省略括号。
参数类型
如果编译器无法从lambda匹配的函数接口方法推断参数类型,则有时可能需要为lambda表达式指定参数类型。不用担心,编译器会在这种情况下告诉我们。这是一个Java lambda参数类型示例:
(Car car) -> System.out.println("The car is: " + car.getName());
如我们所见,car
参数的类型(Car
)写在参数名称本身的前面,就像在其他方法中声明参数或者对接口进行匿名实现时一样。
Java 11中的var参数类型
从Java 11开始,我们可以使用var
关键字作为参数类型。关键字var是在Java 10中作为局部变量类型推断引入的。从Java 11开始,var
也可以用于lambda参数类型。这是在lambda表达式中使用Javavar
关键字作为参数类型的示例:
Function<String, String> toLowerCase = (var input) -> input.toLowerCase();
上面用var关键字声明的参数的类型将推断为String的类型,因为变量的类型声明的通用类型设置为Function <String,String>,这意味着该参数函数的类型和返回类型是字符串。
Lambda函数体
Lambda表达式的主体以及它表示的函数/方法的主体在lambda声明中的->
右边指定:这是一个示例:
(oldState, newState) -> System.out.println("State changed")
如果lambda表达式需要包含多行,则可以将lambda函数主体括在Java中在声明其他方法时也需要的" {}"括号内。这是一个例子:
(oldState, newState) -> { System.out.println("Old state: " + oldState); System.out.println("New state: " + newState); }
从Lambda表达式返回值
我们可以从Java lambda表达式返回值,就像可以从方法中返回值一样。我们只需向lambda函数主体添加一个return语句,如下所示:
(param) -> { System.out.println("param: " + param); return "return value"; }
如果lambda表达式所做的只是计算一个返回值并将其返回,则可以用更短的方式指定返回值。代替这个:
(a1, a2) -> { return a1 > a2; }
你可以写:
(a1, a2) -> a1 > a2;
然后,编译器会发现表达式" a1> a2"是lambda表达式的返回值(因此将lambda表达式命名为表达式会返回某种值)。
Lambda作为对象
Java lambda表达式本质上是一个对象。我们可以将lambda表达式分配给变量并将其传递,就像处理任何其他对象一样。这是一个例子:
public interface MyComparator { public boolean compare(int a1, int a2); }
MyComparator myComparator = (a1, a2) -> return 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函数主体外部声明的String
变量的更新版本:
String myString = "Test"; MyFactory myFactory = (chars) -> { return myString + ":" + new String(chars); };
如我们所见,lambda主体现在引用了在lambda主体外部声明的局部变量" myString"。只有在被引用的变量是"有效最终"的情况下才有可能,这意味着它在赋值后不会更改其值。如果myString
变量的值稍后更改,则编译器会抱怨从lambda主体内部对其的引用。
实例变量捕获
Lambda表达式还可以捕获创建Lambda的对象中的实例变量。这是显示示例:
public class EventConsumerImpl { private String name = "MyConsumer"; public void attach(MyEventProducer eventProducer){ eventProducer.listen(e -> { System.out.println(this.name); }); } }
请注意lambda主体中对" this.name"的引用。这捕获了封闭的EventConsumerImpl
对象的name
实例变量。甚至可以在捕获实例变量后更改其值,该值将反映在lambda内部。
" this"的语义实际上是Java lambda与接口的匿名实现不同的领域之一。匿名接口实现可以具有自己的实例变量,这些实例变量通过" this"引用进行引用。但是,lambda不能拥有自己的实例变量,因此this
始终指向封闭的对象。
注意:事件消费者的上述设计不是特别优雅。我只是这样做,以便能够说明实例变量的捕获。
静态变量捕获
Java lambda表达式还可以捕获静态变量。这并不奇怪,因为只要可以访问静态变量(打包作用域的或者公共的),Java应用程序中的任何地方都可以访问静态变量。
这是一个示例类,该类创建一个lambda,该lambda从lambda主体内部引用静态变量:
public class EventConsumerImpl { private static String someStaticVar = "Some text"; public void attach(MyEventProducer eventProducer){ eventProducer.listen(e -> { System.out.println(someStaticVar); }); } }
lambda捕获到静态变量后,它的值也可以更改。
同样,上述类设计有点荒谬。不要对此考虑太多。该类主要用于向我们显示lambda可以访问静态变量。
方法参考为Lambdas
如果lambda表达式所做的只是用传递给lambda的参数来调用另一个方法,则Java lambda实现提供了一种表达该方法调用的较短方法。首先,这是一个示例单函数接口:
public interface MyPrinter{ public void print(String s); }
以下是创建实现MyPrinter接口的Java lambda实例的示例:
MyPrinter myPrinter = (s) -> { System.out.println(s); };
由于lambda主体仅由一个语句组成,因此我们实际上可以省略括号" {}"。另外,由于lambda方法只有一个参数,因此我们可以省略该参数周围的括号()。这是生成的lambda声明的外观:
MyPrinter myPrinter = s -> System.out.println(s);
由于所有lambda主体所做的工作都是将字符串参数转发给System.out.println()
方法,因此我们可以将上述lambda声明替换为方法引用。以下是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()方法的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);