从熟悉的例子中熟悉 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();

For the Predicate interface it is:

boolean test(T t);

And for the Consumer interface it is:

void accept(T t);

Implementing the Right Method with a Lambda Expression

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

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

此语法由三个元素组成:

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

让我们看看这方面的例子。 假设你需要一个 Predicate返回 true对于恰好有 3 个字符的字符串。

  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;

这将我们引向第一个好的做法:保持 lambda 的简短,以便它们只是一行简单易读的代码。

实施一个 Consumer<String>

在某些时候,人们可能会想走捷径。 您会听到开发人员说“消费者接受一个对象并且什么都不返回”。 或者“当字符串正好有三个字符时,谓词为真”。 大多数情况下,lambda 表达式、它实现的抽象方法和包含此方法的函数接口之间存在混淆。

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

让我们编写一个消耗 String并打印在 System.out. 语法可以是这样的:

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

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

实现一个 Runnable

实施一个 Runnable结果写了一个实现 void run(). 这个参数块是空的,所以应该用括号写。 记住:只有当你有一个参数时你才能省略括号,这里我们有零。

所以让我们写一个 runnable 告诉我们它正在运行:

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

调用 Lambda 表达式

让我们回到之前的 Predicate例如,假设该谓词已在方法中定义。 如何使用它来测试给定字符串的长度是否确实为 3?

好吧,尽管您使用了用于编写 lambda 的语法,但您需要记住这个 lambda 是接口的一个实例 Predicate. 这个接口定义了一个方法叫做 test()这需要一个 String并返回一个 boolean.

让我们把它写成一个方法:

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接口定义了这个方法 boolean test(String),调用中定义的方法是完全合法的 Predicate通过类型变量 Predicate. 乍一看这可能令人困惑,因为这个谓词变量看起来不像它定义了方法。

请耐心等待,有更好的方法来编写此代码,您将在本教程后面看到。

因此,每次编写 lambda 时,都可以调用此 lamdba 正在实现的接口上定义的任何方法。 调用抽象方法将调用 lambda 本身的代码,因为此 lambda 是该方法的实现。 调用默认方法将调用接口中编写的代码。 lambda 无法覆盖默认方法。

捕捉本地价值

一旦习惯了它们,编写 lambda 表达式就变得很自然了。 它们很好地集成在 Collections Framework、Stream 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);
    }
}

即使此代码看起来不错,尝试编译它也会在使用时出现以下错误 totalPrice在这 Consumer执行:

lambda 表达式中使用的变量应该是 final 或有效 final

原因如下:lambda 不能修改在其主体之外定义的变量。 他们可以阅读它们,只要它们是 final,即不可变。 这个访问变量的过程称为 捕获 :lambdas 不能 捕获 变量,它们只能 捕获 值。 最终变量实际上是一个值。

您已经注意到错误消息告诉我们变量可以是 final ,这是 Java 语言中的一个经典概念。 它还告诉我们变量可以是 有效的 final 。 这个概念是在 Java SE 8 中引入的:即使你没有显式声明一个变量 final,编译器可能会为您完成。 如果它发现这个变量是从 lambda 读取的,并且你没有修改它,那么它会很好地添加 final为你声明。 当然这是在编译后的代码中完成的,编译器不会修改你的源代码。 此类变量不称为 final ; 它们被称为 有效的最终 变量。 这是一个非常有用的功能。

具体例子

下面这段代码如果你项目中有用 stream 编程那肯定很熟悉,有一个 Student 的 list,你想把它转换成一个 map,key 是 student 对象的 id,value 就是 student 对象本身。

List<Student> studentList = gen();
Map<String, Student> map = studentList .stream()
        .collect(Collectors.toMap(Student::getId, a -> a, (a, b) -> a));

把 Lamda 表达式的部分提取出来。

Collectors.toMap(Student::getId, a -> a, (a, b) -> a)

由于我们还没见过 :: 这种形式,先打回原样,这里只是让你预热一下。

Collectors.toMap(a -> a.getId(), a -> a, (a, b) -> a)

为什么它被写成这个样子呢?我们看下 Collectors.toMap 这个方法的定义就明白了。

public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends U> valueMapper,
        BinaryOperator<U> mergeFunction) 
{
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}

看,入参有三个,分别是Function,Function,BinaryOperator,其中 BinaryOperator 只是继承了 BiFunction 并扩展了几个方法,我们没有用到,所以不妨就把它当做BiFunction

还记得 Function 和 BiFunction 吧?

Function R apply(T t)

BiFunction R apply(T t, U u)

那就很容易理解了。

第一个参数**a -> a.getId()**就是 R apply(T t) 的实现,入参是 Student 类型的对象 a,返回 a.getId()

第二个参数a -> a也是 R apply(T t) 的实现,入参是 Student 类型的 a,返回 a 本身

第三个参数**(a, b) -> a**是 R apply(T t, U u) 的实现,入参是Student 类型的 a 和 b,返回是第一个入参 a,Stream 里把它用作当两个对象 a 和 b 的 key 相同时,value 就取第一个元素 a

其中第二个参数 a -> a 在 Stream 里表示从 list 转为 map 时的 value 值,就用原来的对象自己,你肯定还见过这样的写法。

Collectors.toMap(a -> a.getId(), Function.identity(), (a, b) -> a)

为什么可以这样写,给你看 Function 类的全貌你就明白了。

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t); 
    ...
    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

看到了吧,identity 这个方法,就是帮我们把表达式给实现了,就不用我们自己写了,其实就是包了个方法。这回知道一个函数式接口,为什么有好多还要包含一堆默认方法和静态方法了吧?就是干这个事用的。

我们再来试一个,Predicate 里面有这样一个默认方法。

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }
}

它能干嘛用呢?我来告诉你,如果没有这个方法,有一段代码你可能会这样写。

Predicate<String> p = 
    s -> (s != null) && 
    !s.isEmpty() && 
    s.length() < 5;

如果利用上这个方法,就可以变成如下这种优雅形式。

Predicate<String> nonNull = s -> s != null;
Predicate<String> nonEmpty = s -> s.isEmpty();
Predicate<String> shorterThan5 = s -> s.length() < 5;

Predicate<String> p = nonNull.and(nonEmpty).and(shorterThan5);

自行体会吧。

方法引用

那我们回过头再看刚刚的 Student::getId 这种写法。当方法体中只有一个方法调用时,就可以作这样的简化。

比如这个 a -> a.getId() 就只是对 Student 对象上 getId() 这个方法的调用,那么就可以写成 Student::getId 这种形式。

再看几个例子

Function<String, Integer> toLength = s -> s.length();
Function<String, Integer> toLength = String::length;

Function<User, String> getName = user -> user.getName();
Function<String, Integer> toLength = User::getName;

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

如果是构造方法的话,也可以简化。

Supplier<List<String>> newListOfStrings = () -> new ArrayList<>();
Supplier<List<String>> newListOfStrings = ArrayList::new;

总结

学会理解和写 Lambda 表达式,别忘了最开始的三步。

1. 确认 Lamda 表达式的类型

2. 找到要实现的方法

3. 实现这个方法

Lamda 表达式的类型就是函数式接口,要实现的方法就是函数式接口里那个唯一的抽象方法,实现这个方法的方式就是参数块 + 小箭头 + 方法体,其中参数块和方法体都可以一定程度上简化它的写法。

附上参考地址:Writing Your First Lambda Expression

原来 Lambda 表达式是这样写的 (公众号:低并发编程)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值