Java 8 Lambda 表达式

Lambda 是啥玩意

简单来讲,Lambda 就是一个匿名的方法,就这样,没啥特别的。它采用一种很是简洁的方式来定义方法。当你想传递可复用的方法片断时,匿名方法很是有用。例如,将一个方法传递给另一个方法。html

Tips
其实不少主流语言早已支持 lambda 表达式,例如,Scala,C#,Objective-C,Ruby,C++(11), Python等等。因此也不是啥新玩意儿。java

匿名方法 VS 匿名类

须要谨记一点,在 Java 里,匿名方法和匿名类并非相同的。匿名类仍然须要实例化对象,匿名类虽然没有明确的名字,但它只有是一个对象时才可以使用。
而匿名方法并不须要给它分配实例,方法与做用的数据分离,而对象与它所做用的数据密切相关。数组

Java 中的 Lambda 表达式

在 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 表达式实现相同的功能:oracle

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

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

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

一些理论上的区别

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

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

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<Integer>

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中存在的接口例如RunnableCallable

@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包中增长了一些新的接口,例如,FunctionPredicate

方法引用

简单来讲,方法引用就是 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());
  }
}

若是咱们想复用实例化的功能,咱们能够抽取出一个新的方法initialisefactory建立对象。

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::staticMethodName String::valueOf
特定对象的实例方法引用 object::instanceMethodName x::toString
特定类型的任意对象的实例方法引用 Class::instanceMethodName String::toString
构造方法引用 ClassName::new String::new

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

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

目前为止,Labmbda 的主要内容已经介绍完毕。

相关文章
相关标签/搜索