Java 中的 Lambda 表达式

1,773 阅读14分钟

在 Java 8之前,一个实现了只有一个抽象方法的接口的匿名类看起来更像Lambda 表达式。下面的代码中,anonymousClass方法调用waitFor方法,参数是一个实现接口的Condition类,实现的功能为,当满足某些条件,Server 就会关闭。 下面的代码是典型的匿名类的使用。

void anonymousClass() {
    final Server server = new HttpServer();
    waitFor(new Condition() {
        @Override
        public Boolean isSatisfied() {
            return !server.isRunning();
        }
    }

下面的代码用 Lambda 表达式实现相同的功能:

void closure() { 
     Server server = new HttpServer();
     waitFor(() -> !server.isRunning()); 
 }

其实,上面的waitFor方法,更接近于下面的代码的描述:

class WaitFor {
    static void waitFor(Condition condition) throws   
    InterruptedException {
        while (!condition.isSatisfied())
            Thread.sleep(250);
    }
}

一些理论上的区别 实际上,上面的两种方法的实现都是闭包,后者的实现就是Lambda 表示式。这就意味着两者都需要持有运行时的环境。在 Java 8 之前,这就需要把匿名类所需要的一切复制给它。在上面的例子中,就需要把 server 属性复制给匿名类。

因为是复制,变量必须声明为 final 类型,以保证在获取和使用时不会被改变。Java 使用了优雅的方式保证了变量不会被更新,所以我们不用显式地把变量加上 final 修饰。

Lambda 表达式则不需要拷贝变量到它的运行环境中,从而 Lambda 表达式被当做是一个真正的方法来对待,而不是一个类的实例。

Lambda 表达式不需要每次都要被实例化,对于 Java 来说,带来巨大的好处。不像实例化匿名类,对内存的影响可以降到最小。

总体来说,匿名方法和匿名类存在以下区别:

类必须实例化,而方法不必; 当一个类被新建时,需要给对象分配内存; 方法只需要分配一次内存,它被存储在堆的永久区内; 对象作用于它自己的数据,而方法不会; 静态类里的方法类似于匿名方法的功能。

一些具体的区别 匿名方法和匿名类有一些具体的区别,主要包括获取语义和覆盖变量。

获取语义 this 关键字是其中的一个语义上的区别。在匿名类中,this 指的是匿名类的实例,例如有了内部类为 Foo$InnerClass,当你引用内部类闭包的作用域时,像Foo.this.x的代码看起来就有些奇怪。 在 Lambda 表达式中,this 指的就是闭包作用域,事实上,Lambda 表达式就是一个作用域,这就意味着你不需要从超类那里继承任何名字,或是引入作用域的层级。你可以在作用域里直接访问属性,方法和局部变量。 例如,下面的代码中,Lambda 表达式可以直接访问firstName变量。

public class Example {
    private String firstName = "Tom";

    public void example() {
        Function<String, String> addSurname = surname -> {
            // equivalent to this.firstName
            return firstName + " " + surname;  // or even,   
        };
    }
}

这里的firstName就是this.firstName的简写。 但是在匿名类中,你必须显式地调用firstName,

public class Example {
    private String firstName = "Jerry";

    public void anotherExample() {
        Function<String, String> addSurname = new Function<String,  
        String>() {
            @Override
            public String apply(String surname) {
                return Example.this.firstName + " " + surname;   
            }
        };
    }
}

覆盖变量 在 Lambda 表达式中,

public class ShadowingExample {

    private String firstName = " Tim";

    public void shadowingExample(String firstName) {
        Function<String, String> addSurname = surname -> {
            return this.firstName + " " + surname;
        };
    }
}

因为 this 在Lambda 表达式中,它指向的是一个封闭的作用域,所以this.firstName对应的值是“Tim”,而不是跟它同名的参数的值。如果去掉this,那么引用的则是方法的参数。

在上面的例子中,如果用匿名类来实现的话,firstName指的就是方法的参数;如果想访问最外面的firstName,则使用Example.this.firstName。

public class ShadowingExample {

    private String firstName = "King";

    public void anotherShadowingExample(String firstName) {
        Function<String, String> addSurname = new Function<String,  
        String>() {
            @Override
            public String apply(String surname) {
                return firstName + " " + surname;
            }
        };
    }
}

Lambda 表达式基本语法 Lambda 表达式基本上就是匿名函数块。它更像是内部类的实例。例如,我们想对一个数组进行排序,我们可以使用Arrays.sort方法,它的参数是Comparator接口,类似于下面的代码。

Arrays.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        return first.compareTo(second);
    }
});

参数里的Comparator实例就是一个抽象片段,本身没有别的。在这里只有在 sort 方法中被使用。 如果我们用新的语法来替换,用 Lambda 表达式的方式来实现:

Arrays.sort(numbers, (first, second) -> first.compareTo(second));

这种方式更加简洁,实际上,Java 把它当做Comparator类的实例来对待。如果我们把 sort 的第二个参数从 Lambda 表达式中抽取出来,它的类型为Comparator。

Comparator<Integer> ascending = (first, second) -> first.compareTo(second);
Arrays.sort(numbers, ascending);

语法分解

你可以把单一的抽象方法转换成 Lambda 表达式。 举例,如果我们有一个接口名为Example,里面只有一个抽象方法apply,该抽象方法返回某一类型。

interface Example {
     R apply(A args);
}

我们可以匿名实现此接口里的方法:

new Example() {
    @Override
    public R apply(A args) {
        body
    }
};

转换成 Lambda 表达式的话,我们去掉实例和声明,去掉方法的细节,只保留方法的参数列表和方法体。

(args) {
    body
}

我们引入新的符号(->)来表示 Lambda 表达式。

(args) -> {
    body
}

拿之前排序的方法为例,首先我们用匿名类来实现:

Arrays.sort(numbers, new Comparator<Integer>() {
    @Override
    public int compare(Integer first, Integer second) {
        return first.compareTo(second);
    }
});

下一步,去掉实例和方法签名:

Arrays.sort(numbers, (Integer first, Integer second) {
    return first.compareTo(second);
});

引用 Lambda 表达式:

Arrays.sort(numbers, (Integer first, Integer second) -> {
    return first.compareTo(second);
});

完成!但有些地方可以进一步优化。你可以去掉参数的类型,编译器已经足够聪明知道参数的类型。

Arrays.sort(numbers, (first, second) -> {
    return first.compareTo(second);
});

如果是一个简单的表达式的话,例如只有一行代码,你可以去掉方法体的大括号,如果有返回值的话,return 关键字也可以去掉。

Arrays.sort(numbers, (first, second) -> first.compareTo(second));

如果Lambda 只有一个参数的话,参数外面的小括号也可以去掉。

(x) -> x + 1

去掉小括号后,

x -> x + 1

下一步我们做下总结,

(int x, int y) -> { return x + y; }
(x, y) -> { return x + y; }
(x, y) -> x + y; x -> x * 2
() -> System.out.println("Hello");
System.out::println;

第一个方式是完整的 Lambda 的声明和使用的方式,不过有些冗余,其实,参数的类型可以省略; 第二个方式是去掉参数类型的 Lambda 表达式; 第三个方式是,如果你的方法体只有一行语句,你可以直接省略掉大括号和 return 关键字; 第四个方式是没有参数的 Lambda 表达式; 第五个方式是Lambda 表达式的变种:是Lambda 表达式的一种简写,称为方法引用。例如:

 System.out::println;

实际上它是下面Lambda 表达式的一种简写:

(value -> System.out.prinltn(value)

深入 Lambda表达式

函数式接口

Java 把 Lambda表达式当作是一个接口类型的实例。它把这种形式被称之为函数式接口。一个函数式接口就是一个只有单一方法的接口,Java把这种方法称之为“函数式方法”,但更常用的名字为单一抽象方法(single abstract method" 或 SAM)。例如JDK中存在的接口例如Runnable和Callable。

@FunctionalInterface

Oracle 引入了一个新的注解为@FunctionalInterface, 用来标识一个接口为函数式接口。它基本上是用来传达这一用途,除此而外,编辑器还会做一些额外的检查。 比如,下面的接口:

public interface FunctionalInterfaceExample {
    // compiles ok
}

如果加上@FunctionalInterface注解,则会编译错误:

@FunctionalInterface // <- error here
    public interface FunctionalInterfaceExample {
      // doesn't compile
}

编译器就会报错,错误的详细信息为“Invalid '@FunctionalInterface' annotation; FunctionalInterfaceExample is not a functional interface”。意思是没有定义一个单一的抽象方法。 而如果我们定义了两个抽象方法会如何?

@FunctionalInterface
public interface FunctionalInterfaceExample {
    void apply();
    void illegal(); // <- error here
}

编译器再次报错,提示为"multiple, non-overriding abstract methods were found"。所以,一旦使用了此注解,则在接口里只能定义一个抽象方法。

而现在有这样一种情况,如歌一个接口继承了另一个接口,会怎么办?我们创建一个新的函数式接口为A,定义了另一个接口B,B继承A,则B仍然是一个函数式接口,它继承了A的apply方法。

@FunctionalInterface
interface A {
    abstract void apply();
}

interface B extends A {

如果你想看起来更加清晰,可以复写父类的方法:

@FunctionalInterface
interface A {
    abstract void apply();
}

interface B extends A {
    @Override
    abstract void apply();
}

我们可以用下面的代码来测试一下上面的两个接口是否为函数式接口:

@FunctionalInterface
public interface A {
    void apply();
}

public interface B extends A {
    @Override
    void apply();
}

public static void main(String... args) {
   A a = () -> System.out.println("A");
   B b = () -> System.out.println("B");
   a.apply(); // 打印:A
   b.apply(); // 打印:B
}

如果B接口继承了A接口,那么在B接口中就不能定义新的方法了,否则编译器会报错。

除了这些,在Java 8 中接口有了一些新的改进:

可以添加默认方法; 可以包含静态接口方法; 在java.util.function包中增加了一些新的接口,例如,Function 和 Predicate。

方法引用

简单来说,方法引用就是 Lambda 表达式的一种简写。当你创建一个 Lambda 表达式时,你创建了一个匿名方法并提供方法体,但你使用方法引用时,你只需要提供已经存在的方法的名字,它本身已经包含方法体。 它的基本语法如下;

Class::method

或一个更加简洁明了的例子:

String::valueOf

"::"符号前面表示的是目标引用,后面表示方法的名字。所以,在上面的例子,String 类作为目标类,用来寻找它的方法valueOf,我们指的就是 String 类上的静态方法。

public static String valueOf(Object obj) { ... }

"::"称之为定界符,当我们使用它的时候,只是用来引用要使用的方法,而不是调用方法,所以不能在方法后面加()。

String::valueOf(); // error

你不能直接调用方法引用,只是用来替代 Lambda 表达式,所以,哪里使用 Lambda 表达式了,哪里就可以使用方法引用了。 所以,下面的代码并不能运行:

public static void main(String... args) {
    String::valueOf;
}

这是因为该方法引用不能转化为Lambda 表达式,因为编译器没有上下文来推断要创建哪种类型的Lambda。 我们知道这个引用其实是等同于下面的代码:

(x) -> String.valueOf(x)

但编译器还不知道。虽然它可以知道一些事情。它知道,作为一个Lambda,返回值应该是字符串类型,因为valueOf方法的返回值为字符串类型。但它不知道作为论据需要提供什么信息。我们需要给它一点帮助,给它更多的上下文信息。 下面我们创建一个函数式接口Conversion,

@FunctionalInterface
interface Conversion {
    String convert(Integer number);
}

接下来我们需要创建一个场景去使用这个接口作为一个 Lambda,我们定义了下面的方法:

public static String convert(Integer number, Conversion function) {
    return function.convert(number);
}

其实,我们已经给编译器提供了足够多的信息,可以把一个方法引用转换成一个等同的 Lambda。当我们调用convert方法时,我们可以把如下代码传递给 Lambda。

convert(100, (number) -> String.valueOf(number));

我们可以用把上面的 Lambda 替换为方法引用,

convert(100, String::valueOf);

另一种方式是我们告诉编译器,把引用分配给一个类型:

Conversion b = (number) -> String.valueOf(number);

用方法引用来表示:

Conversion b = String::valueOf

方法引用的种类

在 Java 中,有四种方法引用的类型:

构造方法引用; 静态方法引用: 两种实例方法引用。 最后两个有点混乱。第一种是特定对象的方法引用,第二个是任意对象的方法引用,而是特定类型的方法引用。区别在于你想如何使用该方法,如果你事先并不知道有没有实例。

构造方法引用

构造方法的基本引用如下:

String::new

它会创建一个 Lambda 表达式,然后调用String 无参的构造方法。 它实际上等同于:

() -> new String()

需要注意的是构造方法引用没有括号,它只是引用,并不是调用,上面的例子只是引用了 String类的构造方法,并没有真正去实例化一个字符串对象。 接下来我们看一个实际应用构造方法引用的例子。 看先的例子,循环十遍为 list 增加对象。

public void usage() {
    List<Object> list = new ArrayList<>();
    for (int i = 0; i < 10; i++) {
        list.add(new Object());
  }
}

如果我们想复用实例化的功能,我们可以抽取出一个新的方法initialise用factory创建对象。

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, ...);
}

private void initialise(List<Object> list, Factory<Object> factory){
    for (int i = 0; i < 10; i++) {
        list.add(factory.create());
    }
 }

Factory是一个函数式接口,包含一个create方法,此方法返回 Object 对象,我们可以用 Lambda 的方式向 list 中添加对象。

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, () -> new Object());
}

或者我们用构造方法引用的方式来替换:

public void usage() {
    List<Object> list = new ArrayList<>();
    initialise(list, Object::new);
}

上面的方法其实还有待改进,上面只是创建 Object 类型的对象,我们可以增加泛型,实现可以创建更多类型的方法。

public void usage() {
    List<String> list = new ArrayList<>();
    initialise(list, String::new);
}

private <T> void initialise(List<T> list, Factory<T> factory) {
    for (int i = 0; i < 10; i++) {
        list.add(factory.create());
    }
}

到现在为知,我们演示的都是无参的构造方法的引用,如果是带有参数的构造方法的引用该如何处理呢? 当有多个构造函数时,使用相同的语法,但编译器计算出哪个构造函数是最佳匹配。它基于目标类型和推断功能接口,它可以用来创建该类型。 例如,我们有个 Person 类,它有一个多个参数的构造方法。

class Person {
    public Person(String forename, String surname, LocalDate    
    birthday, Sex gender, String emailAddress, int age) {
      // ...
    }

回到上面的例子,我们可以如下使用:

initialise(people, () -> new Person(forename, surname, birthday,
                                    gender, email, age));

但是如果想使用这个构造方法引用,则需要 Lambda 表达式提供如下参数:

initialise(people, () -> new Person(forename, surname, birthday,
                                    gender, email, age));

特定对象的方法引用 下面是特定对象的方法引用的例子:

x::toString

x就是我们想要得到的对象。它等同于下面的Lambda 表达式。

() -> x.toString()

这种方法引用可以为我们提供便利的方式在不同的函数式接口类型中进行切换。看例子:

Callable<String> c = () -> "Hello";

Callable的方法为call,当被调用时返回“Hello”。 如果我们有另外一个函数式接口Factory,我们可以使用方法引用的方式来转变Callable这个函数式接口。

Factory<String> f = c::call;

我们可以重新创建一个 Lambda表达式,但是这个技巧是重用预定义的Lambda的一个有用的方式。 将它们分配给变量并重用它们以避免重复。 我们有下面一个例子:

public void example() {
    String x = "hello";
    function(x::toString);
}

这个例子中方法引用使用了闭包。他创建了一个 Lambda用来调用x对象上的toString方法。 上面function方法的签名和实现如下所示:

public static String function(Supplier<String> supplier) {
    return supplier.get();
}

函数式接口Supplier的定义如下:

@FunctionalInterface
public interface Supplier<T> {
  T get();
}

当使用此方法时,它通过get方法返回一个字符串,而且这是唯一的在我们的结构中获取字符串的方式。它等同于:

public void example() {
  String x = "";
  function(() -> x.toString());
}

需要注意的是,这里的 Lambda 表达式没有参数。这表明x变量在Lambda的局部作用域里是不可用的,如果可用必须要放在它的作用域之外。我们必须要掩盖变量x。 如果用匿名类来实现的话,应该是下面的样子,这些需要主意,x变量是如何传递的。

public void example() {
    String x = "";
    function(new Supplier<String>() {
        @Override
        public String get() {
            return x.toString(); // <- closes over 'x'
        }
    });
}

任意对象的实例方法引用(实例随后提供) 最后一种类型的实例方法引用的格式是这样的:

Object::toString

尽管在“::”左边指向的是一个类(有点类似于静态方法引用),实际上它是指向一个对象,toString方法是Object类上的实例方法,不是静态方法。您可能不使用常规实例方法语法的原因是,还没有引用的实例。 在以前,当我们调用x::toString时,我们是知道x的类型,但是有些情况我们是不知道的,但你仍然可以传递一个方法引用,但是在后面使用此语法时需要提供对应的类型。 例如,下面的表达式等同于x没有限制的类型。

(x) -> x.toString()

有两种不同的实例方法的引用基本是学术上的。有时候,你需要传递一些东西,其他时候,Lambda 的用法会为你提供。 这个例子类似于一个常规的方法引用;它这次调用String 对象的toString方法,该字符串提供给使用 Lambda 的函数,而不是从外部作用域传递的函数。

public void lambdaExample() {
    function("value", String::toString);
}

这个String看起来像是引用一个类,其实是一个实例。是不是有些迷惑,为了能清晰一些,我们需要看一个使用 Lambda 表达式的方法,如下:

public static String function(String value, Function<String, String> function) {
    return function.apply(value);
}

所以,这个 String 实例直接传递给了方法,它看起来像一个完全合格的Lambda。

public void lambdaExample() {
    function("value", x -> x.toString());
}

上面的代码可以简写成String::toString, 它是在说在运行时给我提供对象实例。 如果你想用匿名类展开加以理解,它是这个样子的。参数x是可用的并没有被遮蔽,所以它更像是Lambda 表达式而不是闭包。

public void lambdaExample() {
    function("value", new Function<String, String>() {
      @Override
      // takes the argument as a parameter, doesn't need to close 
      over it
      public String apply(String x) {
        return x.toString();
      }
    });
}

方法引用的总结

Oracle描述了四种类型的方法引用,如下所示。

种类举例
静态方法引用ContainingClass::staticMethodName
特定对象的实例方法引用ContainingObject::instanceMethodName
特定类型的任意对象的实例方法引用ContainingType::methodName
构造方法引用ClassName::new

下面是方法引用的语法和具体的例子。

种类语法举例
静态方法引用Class::staticMethodNameString::valueOf
特定对象的实例方法引用object::instanceMethodNamex::toString
特定类型的任意对象的实例方法引用Class::instanceMethodNameString::toString
构造方法引用ClassName::newString::new

最后,上面的方法引用等同于下面对应的 Lambda 表达式。

种类语法Lambda
静态方法引用Class::staticMethodName(s) -> String.valueOf(s)
特定对象的实例方法引用object::instanceMethodName() -> "hello".toString()
特定类型的任意对象的实例方法引用Class::instanceMethodName(s) -> s.toString()
构造方法引用ClassName::new() -> new String()




本文由樊兔教育图二UR整理发布,樊兔教育是一个泛互联网职业教育平台,官网地址:ftuedu.com/