Java 8 Lambda Expression 学习

时间:2021-11-07 19:15:13

Java 8 Lambda Expression

Lambda表达式是什么

在解释Java Lambda Expression是什么之前,我们先来看看lambda表达式在Java语言中的表示形式。在Java中,每一个Lambda表达式都对应一个类型,(通常是接口类型)。在Java8以前,也即是Java Lambda Expression出现之前,Java API中充斥着很多函数是接口,即:仅仅包含了一个抽象方法的接口。在Java中,每一个该类型的Lambda表达式都会被JVM匹配为这个函数接口中的这个唯一的抽象方法。
注:当一个接口中包含有默认方法时(Java8的新特性–接口默认方法),我们仍然认为该接口是函数式接口方法,因为默认方法并不是抽象方法,接口中仍然仅仅包含唯一一个抽象方法。在Java8中定义函数式接口时可以采用@FunctionalInterface进行注解,这样在编译时,如果编译器发现这个注解的接口中有多于一个以上的抽象方法时就会报错。Java中常见的函数式接口有:
| java.lang.Runnable |
| java.util.Comparator |
| … |
在Java8中新增加的java.util.function包中,也包含了很多常用的函数式接口,具体请参见Java API Doc。
在了解了上面的知识后我们可以来解释什么是Java Lambda Expression了:Lambda表达式又被成为闭包或者匿名函数(闭包的概念主要出自于函数式编程)。
在Java中,我们可以将Lambda表达式看作是任意函数式接口类型的一个匿名实例。Java Lambda表达式的出现,从一定程度上改变了代码的编写方式,提供了更轻量级的语法实现。为了快速理解这一概念我们通过下面的示例代码进行理解:
Java Lambda Expression 出现前的函数式接口编码方式(‘重量级”编程方式)

Collections.sort(list, new Comparator<String>(){
    @Override
    public int compare(String str1, String str2){
        return str1.compareTo(str2);
    }
})

Java Lambda Expression 出现之后的Lambda式编码方式(“轻量级”编码方式)

Collections.sort(list, (str1, str2) -> str1.compareTo(str2));

在使用Java Lambda Expression之后是不是感觉整个编码方式都不对劲了~^-^~。上面的Lambda表达式将会由Java编译器自动推导出参数类型,并将其翻译为对应的方法。最终我们只是用(str1, str2) -> str1.compareTo(str2)这一段表达式就完成了之前需要使用一个匿名类来完成的功能。

Java Lambda Expression的组成形式

lambda表达式的语法由参数列表箭头符号->函数体组成。函数体既可以是一个表达式,也可以是一个语句块:
1.表达式:表达式会被执行然后返回执行结果。
2.语句块:语句块中的语句会被依次执行,就像方法中的语句一样。同样的在执行完所有代码语句后,return语句会把控制权交给匿名方法(Lambda表达式)的调用者。break和continue只能在循环中使用,如果函数体有返回值,那么函数体内部的每一条路径都必须返回值。
注:表达式函数体适合小型lambda表达式,它消除了return关键字,使得语法更加简洁。
lambda表达式也经常会出现在嵌套语义中,例如作为方法的参数。为了使lambda表达式在这些场景下尽可能简洁,可以在使用时去除了不必要的分隔符。不过在某些情况下为了代码的可读性,也可以把它分为多行,然后用括号包起来,就像其它普通表达式一样。例如:

String result = buildString((str, from, end) -> str.subString(from, end));

FileFilter file = (File f) -> f.getName().endWith(".java"); // 表达式函数体

new Thread(() -> {
    // do something
}).start();  // 语句块形式

Java Lambda Expression的类型是怎样确定的

从上面的使用示例中我们可以看到,我们在使用Lambda表达式的时候并没有指定函数式接口的类型,但是Java代码却能够编译通过,同时JVM也能够正确的执行。这时候你是不是纳闷,对于一个Lambda表达式Java编译器是怎么知道它对应的函数式接口的类型的。答案就是:Lambda表达式代表的函数式接口类型是由其Java编译器通过其执行上下文推断出来的。同时这也就意味着同一个Lambda表达式在不同的执行上下文中可以拥有者不同的类型。例如:

Callable<String> call = () -> "do something";  // 代表Callable的实例

PrivilegedAction<String> a = () -> "do something"; // 代表PrivilegedAction的实例

注:编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,这个被期待的类型被称为目标类型。lambda表达式只能出现在目标类型为函数式接口的上下文中。当然,lambda表达式对目标类型也是有要求的。编译器会检查lambda表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面所有条件均满足时,lambda表达式才可以被赋给目标类型T:(Java通过判断方法签名确定两个方法是否构成了方法重载)
1.目标类型T是一个函数式接口
2.Lambda表达式的参数与其目标类型T的方法参数在数量和类型上一一对应
3.Lambda表达式的返回值和与其目标类型T的方法返回值相兼容(Compatible)
4.Lambda表达式内所抛出的异常与其目标类型的抽象方法的throws类型相兼容
同过上面的约定,也进一步简化了Lambda表达式的编写。由于目标类型(函数式接口)已经“知道”Lambda表达式的形式参数(Formal parameter)类型,所以我们在编写Lambda表达式时就没有必要把已知类型再重复一遍。也就是说,lambda表达式的参数类型可以从目标类型中得出。此外,当Lambda的参数只有一个而且它的类型可以被推导得知时,该参数列表外面的括号可以被省略:例如:

// 编译器能够推导出str1和str2的类型是String
Comparator<String> c = (str1, str2) -> str1.compareTo(str2);

// 省略了参数列表外面的括号 
FileFilter java = f -> f.getName().endsWith(".java");

FileFilter file = (File f) -> f.getName().endWith(".java");

Java Lambda Expression 的作用域

友情提示:如果对作用域这个概念不太了解的童鞋们可以参见我的另外一篇博文,或是上*自行搜索学习。
在Java内部类中使用变量名(以及this)时非常容易出错。因为内部类通过继承得到的成员(包括来自Object的方法)可能会把外部类的成员隐藏起来(shadow),同时未限定(unqualified)的this引用会指向内部类自己而非外部类,这时想要在内部中引用外部内的成员时,需要做一些特殊的处理才行。相对于Java inner class,Java Lambda Expression的语义就十分简单:它不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。Java Lambda表达式采用的是词法作用域,也即:Java Lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括Lambda表达式的形式参数)。此外,’this’关键字及其引用在Java Lambda表达式内部和外部也拥有相同的语义。我们可以很方便的访问到外部类中采用final标记的局部变量,实例的字段以及类成员变量等。

public class Demo {
    private String test = "Hello, Java Lambda Expression"; 
    Runnable run1 = () -> { System.out.println(this); };
    Runnable run2 = () -> { System.out.println(toString()); };
    Runnable run3 = () -> { System.out.println(test); };

    public String toString() {  
        return "from console : Hello, Java Lambda Expression !"; 
    }

    public static void main(String[] args) {
        new Demo().run1.run();
        new Demo().run2.run();
        new Demo().run3.run();
    }
}