一、编写您的第一个Lambda表达式

编写Lambda 表达式

在2014年,Java SE 8引入了lambda表达式的概念。如果您还记得Java SE 8发布之前的日子,那么您可能还记得匿名类的概念。也许您听说过lambda表达式是另一种在某些情况下编写匿名类实例的更简单的方法。
如果您不记得那些日子,那么您可能听说过或读过匿名类,并且可能害怕这种晦涩的语法。

好的,好消息是:您不需要通过匿名类来理解如何编写lambda表达式。而且,在许多情况下,由于在Java语言中添加了lambda,您不再需要匿名类了。
编写lambda表达式可以分为三个步骤:

  • 标识要编写的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表达式实现正确的方法

编写实现Predicate的第一个Lambda表达式

现在是最后一部分:写lambda本身。您需要理解的是,您正在编写的lambda表达式是您找到的抽象方法的实现。使用lambda表达式语法,可以很好地将这个实现内联到代码中。

该语法由三个元素组成:

  • 参数块;
  • 一小块ASCII图形,描绘一个箭头:->。注意Java使用了弱箭头(->)而不是粗箭头(=>);
  • 一个代码块,它是方法的主体。

让我们看看这方面的例子。假设您需要一个Predicate的实例,该实例对于恰好包含3个字符的字符串返回true。

  1. 您的lambda表达式的类型是Predicate
  2. 您需要实现的方法是boolean test(String s)

然后编写参数块,简单地复制/粘贴方法的签名:(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的简短,这样它们就只有一行简单的、可读的代码。

实现一个Consumer

在某些时候,人们可能会倾向于走捷径。你会听到开发人员说“消费者接受一个对象,但不返回任何东西”。或者“当字符串正好有三个字符时predicate为真”。大多数时候,在lambda表达式、它实现的抽象方法和包含该方法的函数式接口之间存在混淆。

但是由于函数式接口、它的抽象方法和实现它的lambda表达式是如此紧密地联系在一起,所以这种说法实际上是完全有意义的。只要不引起任何歧义,那就没问题。

让我们编写一个lambda,它使用String并在System.out上打印。语法可以是这样的:

Consumer<String> print = s -> System.out.println(s);

这里我们直接写出了简化版的lambda表达式。

实现一个Runnable

实现一个Runnable就是编写一个void run()的实现。这个参数块是空的,所以应该用括号来写。记住:只有当你有一个参数时,你才能省略括号,这里是0个。

所以让我们写一个runnable来表示它正在运行:

Runnable runnable = () -> System.out.println("I am running");

调用Lambda表达式

让我们回到前面的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表达式?嗯,lambda表达式可以存储在字段中,这个字段可以通过构造函数或setter方法访问。然后,您可能在运行时对象的状态中有一个lambda表达式,而不知道它。

因此,为了保持与现有可序列化类的向后兼容性,序列化lambda表达式是可能的。

你可能感兴趣的:(java,8,lambda,java,lambda,stream)