Lambda 学习记录

2022-06-10 21:39:28 浏览数 (1)

主要介绍如何构建Lambda,它的使用场合,以及如何利用它使代码更简洁。

Lambda——匿名函数

除了允许(命名)函数成为一等值外,Java 8还体现了更广义的将函数作为值的思想,包括Lambda(或匿名函数)。

❑ 行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。

❑ 行为参数化可让代码更好地适应不断变化的要求,减轻未来的工作量。

❑ 传递代码,就是将新行为作为参数传递给方法。但在Java 8之前这实现起来很啰嗦。为接口声明许多只用一次的实体类而造成的啰嗦代码,在Java 8之前可以用匿名类来减少。

❑ Java API包含很多可以用不同行为进行参数化的方法,包括排序、线程和GUI处理。

如何构建Lambda,它的使用场合,以及如何利用它使代码更简洁。

Lambda管中窥豹

可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

代码语言:javascript复制
    // Java8之前的写法
        Comparator<Apple> weight = new Comparator<Apple>() {
            @Override
            public int compare(Apple o1, Apple o2) {
                return o1.getWeight().compareTo(o2.getWeight());
            }
        };

    // Lambda
        Comparator<Apple> weight1 = (a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

    // Lambda
        Comparator<Apple> weight2 = Comparator.comparing(Apple::getWeight);

❑ 参数列表——这里它采用了Comparator中compare方法的参数,两个Apple。

❑ 箭头——箭头->把参数列表与Lambda主体分隔开。

❑ Lambda主体——比较两个Apple的重量。表达式就是Lambda的返回值了。

Java8中有效的Lambda表达式:

Lambda的基本语法是

代码语言:javascript复制
    (parameters) -> expression

或(请注意语句的花括号)

代码语言:javascript复制
    (parameters) -> {statements;}

根据上述语法规则,以下哪个不是有效的Lambda表达式? (1) ()-> {}(2) ()-> "Raoul"(3) ()-> {return "Mario"; }(4) (Integer i)-> return "Alan" i;(5) (String s)-> {"IronMan"; } 答案:只有4和5是无效的Lambda。(1) 这个Lambda没有参数,并返回void。它类似于主体为空的方法:publicvoid run() {}。(2) 这个Lambda没有参数,并返回String作为表达式。(3) 这个Lambda没有参数,并返回String(利用显式返回语句)。(4) return是一个控制流语句。要使此Lambda有效,需要使花括号,如下所示:(Integer i)-> {return "Alan" i; }。(5)“Iron Man”是一个表达式,不是一个语句。要使此Lambda有效,你可以去除花括号和分号,如下所示:(String s)-> "Iron Man"。或者如果你喜欢,可以使用显式返回语句,如下所示:(String s)->{return "IronMan"; }。

Lambda示例:

在哪里以及如何使用Lambda

函数式接口

代码语言:javascript复制
public interface Predicate<T> {
    boolean test(T t);
}

函数式接口就是只定义一个抽象方法的接口。

API中的一些其他函数式接口,如Comparator和Runnable。

用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具体实现的实例)。你用匿名内部类也可以完成同样的事情,只不过比较笨拙:需要提供一个实现,然后再直接内联将它实例化。

代码语言:javascript复制
public class Test {
    public static void main(String[] args) {

        // 使用Lambda
        Runnable r1 = () -> System.out.println("Hello World! 1");

        // 使用匿名类
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World! 2");
            }
        };


        process(r1);
        process(r2);
        process(() -> System.out.println("Hello World! 3"));

    }

    public static void process(Runnable r) {
        r.run();
    }
}
代码语言:javascript复制
Hello World! 1
Hello World! 2
Hello World! 3

函数描述符

函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符。例如,Runnable接口可以看作一个什么也不接受什么也不返回(void)的函数的签名,因为它只有一个叫作run的抽象方法,这个方法什么也不接受,什么也不返回(void)。

Java 8中的常用函数式接口:

Lambda表达式是怎么做类型检查的。这个会在3.5节中详细介绍,编译器是如何检查Lambda在给定上下文中是否有效的。现在,只要知道Lambda表达式可以被赋给一个变量,或传递给一个接受函数式接口作为参数的方法就好了,当然这个Lambda表达式的签名要和函数式接口的抽象方法一样。

“为什么只有在需要函数式接口的时候才可以传递Lambda呢? ”语言的设计者也考虑过其他办法,例如给Java添加函数类型。 但是他们选择了现在这种方式,因为这种方式自然且能避免语言变得更复杂。 此外,大多数Java程序员都已经熟悉了具有一个抽象方法的接口的理念(例如事件处理)。

以下哪些是使用Lambda表达式的有效方式?

答案:只有1和2是有效的。 第一个例子有效,是因为Lambda()-> {}具有签名()-> void,这和Runnable中的抽象方法run的签名相匹配。请注意,此代码运行后什么都不会做,因为Lambda是空的! 第二个例子也是有效的。事实上,fetch方法的返回类型是Callable。Callable基本上就定义了一个方法,签名是()-> String,其中T被String代替了。因为Lambda()-> "Trickyexample; -)"的签名是()-> String,所以在这个上下文中可以使用Lambda。 第三个例子无效,因为Lambda表达式(Apple a)-> a.getWeight()的签名是(Apple)->Integer,这和Predicate:(Apple)-> boolean中定义的test方法的签名不同。

关于 @FunctionalInterface

如果去看看新的Java API,会发现函数式接口带有@FunctionalInterface的标注这个标注用于表示该接口会设计成一个函数式接口。

如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。例如,错误消息可能是“Multiple non-overriding abstract methods foundin interface Foo”,表明存在多个抽象方法。请注意,@FunctionalInter-face不是必需的,但对于为此设计的接口而言,使用它是比较好的做法。它就像是@Override标注表示方法被重写了。

可以把它看作是一个标识。

把Lambda付诸实践:环绕执行模式

资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些处理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。这就是所谓的环绕执行(execute around)模式。

例如,在以下代码中,高亮显示的就是从一个文件中读取一行所需的模板代码(注意你使用了Java 7中的带资源的try语句,它已经简化了代码,因为你不需要显式地关闭资源了):

代码语言:javascript复制
    public static String processFiles() {
        try {
            BufferedReader br = new BufferedReader(new FileReader("data.txt"));
            return br.readLine();
        }
    }

最终效果:

Lambda仅可用于上下文是函数式接口的情况。

  1. 创建一个接口,一个能匹配BufferedReader-> String,还可以抛出IOException异常的接口。 @FunctionalInterface public interface BufferReaderProcessor { String process(BufferedReader br) throws IOException; }
  2. 把这个接口作为新的processFile方法的参数。 public static String processFiles(BufferReaderProcessor p) { try { BufferedReader br = new BufferedReader(new FileReader("data.txt")); return p.process(br); } }

任何BufferedReader-> String形式的Lambda都可以作为参数来传递,因为它们符合BufferedReaderProcessor接口中定义的process方法的签名。

现在只需要一种方法在processFile主体内执行Lambda所代表的代码。

请记住,Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。因此,可以在processFile主体内,对得到的BufferedReaderProcessor对象调用process方法执行处理:

现在可以通过传递不同的Lambda重用processFile方法,并以不同的方式处理文件了。

代码语言:javascript复制
// 处理一行
String res1 = processFiles((BufferedReader br1) -> br1.readLine());

// 处理两行
String res2 = processFiles((br2) -> br2.readLine()   br2.readLine());

使用函数式接口

Lambda和方法引用实战

  1. 使用匿名类
  1. 使用Lambda表达式

可以继续重写为:

Comparator具有一个叫作comparing的静态辅助方法,它可以接受一个Function来提取Comparable键值,并生成一个Comparator对象。 可以写为:

使用方法引用

总结

  • Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常的列表。
  • Lambda表达式让你可以简洁地传递代码。
  • 函数式接口就是仅仅声明了一个抽象方法的接口。
  • 只有在接受函数式接口的地方才可以使用Lambda表达式。
  • Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
  • Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function<T, R>、Supplier、Consumer和BinaryOperator。
  • 为了避免装箱操作,对Predicate和Function<T, R>等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
  • 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
  • Lambda表达式所需要代表的类型称为目标类型。
  • 方法引用让你重复使用现有的方法实现并直接传递它们。
  • Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可 Links: https://cloud.tencent.com/developer/article/2020533

0 人点赞