在2014年,Java SE 8引入了lambda表达式的概念。如果您还记得Java SE 8发布之前的日子,那么您可能还记得匿名类的概念。也许您听说过lambda表达式是另一种在某些情况下编写匿名类实例的更简单的方法。
如果您不记得那些日子,那么您可能听说过或读过匿名类,并且可能害怕这种晦涩的语法。
好的,好消息是:您不需要通过匿名类来理解如何编写lambda表达式。而且,在许多情况下,由于在Java语言中添加了lambda,您不再需要匿名类了。
编写lambda表达式可以分为三个步骤:
这就是它的全部。让我们详细看看这三个步骤。
在Java语言中,所有东西都有一个类型,这个类型在编译时是已知的。因此,总是可以找到lambda表达式的类型。它可以是变量的类型、字段的类型、方法参数的类型或方法的返回类型。
对于lambda表达式的类型有一个限制:它必须是一个功能接口。因此,不实现函数接口的匿名类不能写成lambda表达式。
函数式接口的完整定义有点复杂。此时您需要知道的是,函数式接口是只有一个抽象方法的接口。
您应该意识到,从Java SE 8开始,接口中允许使用具体方法。它们可以是实例方法,在这种情况下,它们被称为默认方法,它们也可以是静态方法。这些方法不计算在内,因为它们不是抽象方法。
我是否需要在接口上添加@FunctionalInterface注释以使其正常工作?
不,你不需要。这个注释可以帮助您确保接口确实是函数式的。如果将此注释放在非函数式接口的类型上,编译器将引发错误。
让我们看一些取自JDK API的示例。我们刚刚从源代码中删除了注释。
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
Runnable接口确实是函数式的,因为它只有一个抽象方法。@FunctionalInterface注释是作为辅助添加的,但并不需要它。
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after) {
// the body of this method has been removed
}
}
Consumer接口也是函数式的:它有一个抽象方法和一个不算在内的默认的具体方法。同样,不需要@FunctionalInterface注释。
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
// the body of this method has been removed
}
default Predicate<T> negate() {
// the body of this method has been removed
}
default Predicate<T> or(Predicate<? super T> other) {
// the body of this method has been removed
}
static <T> Predicate<T> isEqual(Object targetRef) {
// the body of this method has been removed
}
static <T> Predicate<T> not(Predicate<? super T> target) {
// the body of this method has been removed
}
}
Predicate接口稍微复杂一点,但它仍然是一个函数式接口:
至此,您已经确定了需要编写的lambda表达式的类型,好消息是:您已经完成了最困难的部分:其余部分非常机械,也很容易完成。
lambda表达式是这个函数接口中唯一抽象方法的实现。所以找到要实现的正确方法就是找到这个方法。
你可以花点时间在前一段的三个例子中寻找它。
对于Runnable接口,它是:
public abstract void run();
对于Predicate接口,它是:
boolean test(T t);
对于Consumer接口,它是:
void accept(T t);
现在是最后一部分:写lambda本身。您需要理解的是,您正在编写的lambda表达式是您找到的抽象方法的实现。使用lambda表达式语法,可以很好地将这个实现内联到代码中。
该语法由三个元素组成:
让我们看看这方面的例子。假设您需要一个Predicate的实例,该实例对于恰好包含3个字符的字符串返回true。
然后编写参数块,简单地复制/粘贴方法的签名:(String s)。
然后你添加一个小箭头:->。
以及方法的主体。你的结果应该像这样:
Predicate<String> predicate =
(String s) -> {
return s.length() == 3;
};
这个语法可以被简化,这要感谢编译器可以猜出很多东西,所以您不需要编写它们。
首先,编译器知道您正在实现Predicate接口的抽象方法,并且知道该方法接受一个String作为参数。所以(String s)可以简化为(s)。在这种情况下,只有一个参数,你甚至可以更进一步,删除括号。然后参数块就变成了s。如果你有多个参数,或者没有参数,你应该保留括号。
第二,方法体中只有一行代码。在这种情况下,您不需要花括号或return关键字。
最后的语法是这样的:
Predicate<String> predicate = s -> s.length() == 3;
这就引出了第一个好的实践:保持lambdas的简短,这样它们就只有一行简单的、可读的代码。
在某些时候,人们可能会倾向于走捷径。你会听到开发人员说“消费者接受一个对象,但不返回任何东西”。或者“当字符串正好有三个字符时predicate为真”。大多数时候,在lambda表达式、它实现的抽象方法和包含该方法的函数式接口之间存在混淆。
但是由于函数式接口、它的抽象方法和实现它的lambda表达式是如此紧密地联系在一起,所以这种说法实际上是完全有意义的。只要不引起任何歧义,那就没问题。
让我们编写一个lambda,它使用String并在System.out上打印。语法可以是这样的:
Consumer<String> print = s -> System.out.println(s);
这里我们直接写出了简化版的lambda表达式。
实现一个Runnable就是编写一个void run()的实现。这个参数块是空的,所以应该用括号来写。记住:只有当你有一个参数时,你才能省略括号,这里是0个。
所以让我们写一个runnable来表示它正在运行:
Runnable runnable = () -> System.out.println("I am running");
让我们回到前面的Predicate示例,并假设这个predicate 已经在一个方法中定义。如何使用它来测试给定字符串的长度是否确实为3?
好吧,尽管您使用语法来编写lambda,但您需要记住这个lambda是接口Predicate的一个实例。该接口定义了一个名为test的方法,该方法接受一个String对象并返回一个布尔值。
让我们把它写成一个方法:
List<String> retainStringsOfLength3(List<String> strings) {
Predicate<String> predicate = s -> s.length() == 3;
List<String> stringsOfLength3 = new ArrayList<>();
for (String s: strings) {
if (predicate.test(s)) {
stringsOfLength3.add(s);
}
}
return stringsOfLength3;
}
请注意如何定义predicate,就像在前面的示例中所做的那样。因为Predicate接口定义了这个boolean test(String)方法,所以通过Predicate类型的变量调用Predicate中定义的方法是完全合法的。乍一看,这可能令人困惑,因为这个predicate变量看起来不像定义方法。
请耐心听我们讲,还有更好的方法来编写这段代码,您将在本教程的后面看到。
因此,每次编写lambda时,都可以调用这个lambda所实现的接口上定义的任何方法。调用抽象方法将调用lambda本身的代码,因为这个lambda是该方法的实现。调用默认方法将调用在接口中编写的代码。lambda无法覆盖默认方法。
一旦你习惯了,写lambda就会变得很自然。它们很好地集成在集合框架、流API和JDK的许多其他地方。从Java SE 8开始,lambda无处不在,这是最好的。
在使用lambda时存在一些约束,您可能会遇到需要理解的编译时错误。
让我们思考以下代码:
int calculateTotalPrice(List<Product> products) {
int totalPrice = 0;
Consumer<Product> consumer =
product -> totalPrice += product.getPrice();
for (Product product: products) {
consumer.accept(product);
}
}
即使这段代码看起来很漂亮,尝试编译它也会在这个Consumer实现中使用totalPrice时出现以下错误:
在lambda表达式中使用的变量应该是final或有效的final
原因如下:lambda不能修改在其主体之外定义的变量。他们可以读取它们,只要它们是final的,也就是不可变的。这个访问变量的过程称为捕获:lambda不能捕获变量,它们只能捕获值。final变量实际上是一个值。
您已经注意到,错误消息告诉我们变量可以是final,这是Java语言中的一个经典概念。它还告诉我们变量可以是有效的final。这个概念是在Java SE 8中引入的:即使你没有显式地声明变量final,编译器也可以为你做这件事。如果它看到这个变量是从lambda中读取的,并且您没有修改它,那么它将很好地为您添加最终声明。当然,这是在编译代码中完成的,编译器不会修改您的源代码。这样的变量不叫final;它们实际上被称为有效的final变量。这是一个非常有用的特性。
Lambda表达式已经构建为可以序列化。
为什么要序列化lambda表达式?嗯,lambda表达式可以存储在字段中,这个字段可以通过构造函数或setter方法访问。然后,您可能在运行时对象的状态中有一个lambda表达式,而不知道它。
因此,为了保持与现有可序列化类的向后兼容性,序列化lambda表达式是可能的。