JavaLambda表达式

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

Java lambda表达式在Java 8中是新的。Java lambda表达式是Java进入函数式编程的第一步。因此,Java lambda表达式是一个可以在不属于任何类的情况下创建的函数。Java lambda表达式可以像对象一样传递,并根据需要执行。

Java lambda表达式通常用于实现简单的事件侦听器/回调,或使用[Java Streams API]进行功能编程。Java Lambda表达式也经常用于[Java中的功能编程]

image.png

Java Lambdas和单一方法界面

函数式编程通常用于实现事件侦听器。Java中的事件侦听器通常被定义为使用单个方法的Java接口。以下是一个虚构的单方法界面示例:

public interface StateChangeListener {

    public void onStateChange(State oldState, State newState);

}
复制代码

这个Java接口定义了一个方法,每当状态发生变化时(无论观察到什么),都会调用该方法。

在Java 7中,您必须实现此接口才能监听状态更改。想象一下,您有一个名为StateOwner的类,可以注册State Event侦听器。以下是一个例子:

public class StateOwner {

    public void addStateListener(StateChangeListener listener) { ... }

}
复制代码

在Java 7中,您可以使用匿名接口实现添加事件侦听器,如下所示:

image.png

首先创建一个StateOwner实例。然后,StateChangeListener接口的匿名实现作为StateOwner实例上的侦听器添加。

在Java 8中,您可以使用Java lambda表达式添加事件侦听器,如下所示:

image.png

lambda表达式是这一部分:

(oldState, newState) -> System.out.println("State changed")
复制代码

lambda表达式与addStateListener()方法参数的参数类型匹配。如果lambda表达式与参数类型匹配(此处为StateChangeListener接口),则lambda表达式将转换为实现与该参数相同的接口的函数。

Java lambda表达式只能在匹配的类型为单个方法接口时使用。在上面的示例中,lambda表达式用作参数,其中参数类型是StateChangeListener接口。此接口只有一个方法。因此,lambda表达式与该接口成功匹配。

将Lambdas与接口匹配

单个方法接口有时也称为功能接口。将Java lambda表达式与函数接口匹配分为以下步骤:

  • 接口是否只有一个抽象(未实现)的方法?
  • lambda表达式的参数是否与单一方法的参数匹配?
  • lambda表达式的返回类型是否与单个方法的返回类型匹配?

如果这三个问题的答案是肯定的,那么给定的lambda表达式将与接口成功匹配。

具有默认和静态方法的接口

从Java 8中,[Java接口]可以同时包含默认方法和静态方法。默认方法和静态方法都有一个直接在接口声明中定义的实现。这意味着,Java lambda表达式可以使用多个方法实现接口——只要接口只有一个未实现(AKA抽象)方法。

换句话说,即使接口包含默认和静态方法,它仍然是一个功能接口, 只要接口只包含一个未实现(抽象)方法。以下是这个小部分的视频版本:

image.png

以下接口可以使用lambda表达式实现:

image.png

即使此接口包含3种方法,也可以通过lambda表达式实现,因为只有一个方法未实现。以下是实现的外观:

MyInterface myInterface = (String text) -> {
    System.out.print(text);
};
复制代码

Lambda表达式与匿名接口实现

尽管lambda表达式接近匿名接口实现,但有一些差异值得注意。

主要区别在于,匿名接口实现可以有状态(成员变量),而lambda表达式不能。看看这个界面:

public interface MyEventConsumer {

    public void consume(Object event);

}
复制代码

此接口可以使用匿名接口实现实现,如下所示:

image.png

这个匿名的MyEventConsumer实现可以有自己的内部状态。看看这个重新设计:

image.png

请注意,匿名MyEventConsumer实现现在如何具有一个名为eventCount的字段。

lambda表达式不能有这样的字段。因此,lambda的表达被称为无国籍。

Lambda类型推断

在Java 8之前,在进行匿名接口实现时,您必须指定要实现的接口。以下是本文开头的匿名接口实现示例:

image.png

使用lambda表达式,通常可以从周围的代码中推断类型。例如,可以从addStateListener()方法(StateChangeListener接口上的单个方法)的方法声明中推断参数的接口类型。这被称为类型推理。编译器通过在其他地方查找类型来推断参数的类型——在这种情况下,是方法定义。以下是本文开头的示例,显示lambda表达式中没有提到StateChangeListener接口:

stateOwner.addStateListener(
    (oldState, newState) -> System.out.println("State changed")
);
复制代码

在lambda表达式中,参数类型通常也可以推断。在上面的示例中,编译器可以从onStateChange()方法声明中推断其类型。因此,从onStateChange()方法的方法声明中推断出参数oldStatenewState的类型。

Lambda参数

由于Java lambda表达式实际上是方法,lambda表达式可以像方法一样接受参数。前面显示的lambda表达式的(oldState, newState)部分指定lambda表达式的参数。这些参数必须与方法在单个方法界面上的参数匹配。在这种情况下,这些参数必须与StateChangeListener接口的theonStateChangeonStateChange()方法的参数匹配:

public void onStateChange(State oldState, State newState);
复制代码

lambda表达式和方法中的参数数量必须至少匹配。

其次,如果您在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 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表达式中使用Java var关键字作为参数类型的示例:

Function<String, String> toLowerCase = (var input) -> input.toLowerCase();
复制代码

使用上述var关键字声明的参数类型将推断为String类型,因为变量的类型声明的泛型类型设置为Function<String, String>,这意味着Function的参数类型和返回类型是String

Lambda功能体

lambda表达式的主体,因此它所代表的函数/方法的主体,在lambda声明的->右侧指定:示例如下:

 (oldState, newState) -> System.out.println("State changed") 
复制代码

如果您的lambda表达式需要由多行组成,您可以将lambda函数体包含在{ }括号中,Java在其他地方声明方法时也需要这些括号内。以下是一个例子:

image.png

从Lambda表达式返回值

您可以从Java lambda表达式返回值,就像从方法返回一样。您只需向lambda函数体添加返回语句,如下所示:

(param) -> {
    System.out.println("param: " + param);
    return "return value";
  }
复制代码

如果您的lambda表达式所做的只是计算并返回返回值,您可以以更短的方式指定返回值。取而代之的是:

(a1, a2) -> { return a1 > a2; }
复制代码

您可以写:

(a1,a2)-> a1> a2;
复制代码

然后,编译器计算出表达式a1 > a2是lambda表达式的返回值(因此lambda表达式的名称——因为表达式返回某种值)。

Lambdas作为对象

Java lambda表达式本质上是一个对象。您可以为变量分配lambda表达式,并像处理任何其他对象一样传递它。以下是一个例子:

image.png
第一个代码块显示lambda表达式实现的接口。第二个代码块显示了lambda表达式的定义,lambda表达式如何分配给变量,最后如何通过调用lambda表达式实现的接口方法来调用。

可变捕获

在某些情况下,Java lambda表达式能够访问lambda函数体外声明的变量。我这里有这个部分的视频版本:

image.png

Java lambdas可以捕获以下类型的变量:

  • 局部变量
  • 实例变量
  • 静态变量

这些变量捕获中的每一个都将在以下部分中描述。

本地变量捕获

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 lambdas与接口的匿名实现不同的领域之一。匿名接口实现可以有自己的实例变量,这些变量通过this引用引用。然而,lambda不能有自己的实例变量,因此this总是指向封闭对象。

注:上述活动消费者的设计并不特别优雅。我就是这样做的,以便能够说明实例变量捕获。

静态变量捕获

Java lambda表达式还可以捕获静态变量。这并不奇怪,因为静态变量可以在Java应用程序中的任何地方访问,前提是静态变量是可访问的(打包范围或公共的)。

以下是一个示例类,该类创建一个lambda,该lambda引用lambda主体内的静态变量:

image.png

静态变量的值也允许在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);
}
复制代码

这是一个静态方法,我们希望创建一个方法参考:

image.png

最后,这里有一个引用静态方法的Java lambda表达式:

Finder finder = MyClass::doFind;
复制代码

由于Finder.find()MyClass.doFind()方法的参数匹配,因此可以创建一个lambda表达式,实现Finder.find()并引用MyClass.doFind()方法。

参数方法参考

您还可以将其中一个参数的方法引用到lambda。想象一个如下所示的单个函数接口:

public interface Finder {
    public int find(String s1, String s2);
}
复制代码

该接口旨在表示能够搜索s1以查找s2出现的组件。以下是调用String.indexOf()进行搜索的Java lambda表达式示例:

Finder finder = String::indexOf;
复制代码

这相当于lambda的定义:

Finder finder = (s1, s2) -> s1.indexOf(s2);
复制代码

注意快捷版本如何引用单个方法。Java编译器将尝试将引用的方法与第一个参数类型匹配,使用第二个参数类型作为引用方法的参数。

实例方法参考

第三,也可以从lambda定义中引用实例方法。首先,让我们看看单个方法接口定义:

public interface Deserializer {
    public int deserialize(String v1);
}
复制代码

此接口表示能够将String“反序列化”为int的组件。

现在看看这个StringConverter类:

image.png

' 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);
复制代码