Java Lambda Expression

Lambda表达式本质

一个Lambda表达式本质上是一个匿名函数

Lambda表达式出现的原因

匿名类型最大的问题就在于其冗余的语法,而Lambda表达式提供了轻量级的语法,解决了匿名内部类代码冗余的问题。

Lambda表达式语法

lambda表达式的语法由参数列表、箭头符号->和函数体组成。函数体既可以是一个表达式,也可以是一个语句块:

  • 表达式:表达式会被执行然后返回执行结果。
  • 语句块:语句块中的语句会被依次执行,就像方法中的语句一样——
    • return语句会把控制权交给匿名方法的调用者
    • break和continue只能在循环中使用
    • 如果函数体有返回值,那么函数体内部的每一条路径都必须返回值

表达式函数体适合小型lambda表达式,它消除了return关键字,使得语法更加简洁。

lambda表达式也会经常出现在嵌套环境中,比如说作为方法的参数。为了使lambda表达式在这些场景下尽可能简洁,去除了不必要的分隔符。不过在某些情况下我们也可以把它分为多行,然后用括号包起来,就像其它普通表达式一样。

栗子:

1
2
3
4
5
6
7
8
9
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }
FileFilter java = (File f) -> f.getName().endsWith("*.java");
String user = doPrivileged(() -> System.getProperty("user.name"));
new Thread( () -> {
connectToService();
sendNotification();
}).start();

Lambda表达式的目标类型

Lambda表达式的目标类型是由其上下文推导而来

编译器负责推导lambda表达式的类型。它利用lambda表达式所在上下文所期待的类型进行推导,这个被期待的类型被称为目标类型。lambda表达式只能出现在目标类型为函数式接口的上下文中。

lambda表达式对目标类型也是有要求的。编译器会检查lambda表达式的类型和目标类型的方法签名(method signature)是否一致。当且仅当下面所有条件均满足时,lambda表达式才可以被赋给目标类型T:

  • T是一个函数式接口
  • lambda表达式的参数和T的方法参数在数量和类型上一一对应
  • lambda表达式的返回值和T的方法返回值相兼容(Compatible)
  • lambda表达式内所抛出的异常和T的方法throws类型相兼容

由于目标类型(函数式接口)已经“知道”lambda表达式的形式参数(Formal parameter)类型,lambda表达式的参数类型可以从目标类型中得出。

栗子:

1
2
3
4
5
6
ActionListener l = (ActionEvent e) -> ui.dazzle(e.getModifiers());
Callable<String> c = () -> "done";
PrivilegedAction<String> a = () -> "done";
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
FileFilter java = f -> f.getName().endsWith(".java");
button.addActionListener(e -> ui.dazzle(e.getModifiers()));

Lambda表达式词法作用域

在内部类中使用变量名(以及this)非常容易出错。内部类中通过继承得到的成员(包括来自Object的方法)可能会把外部类的成员掩盖(shadow),此外未限定(unqualified)的this引用会指向内部类自己而非外部类。

相对于内部类,lambda表达式的语义就十分简单:它不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。lambda表达式基于词法作用域,也就是说lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括lambda表达式的形式参数)。此外,’this’关键字及其引用在lambda表达式内部和外部也拥有相同的语义。

也就是说在内部类中的 this 是有可能有歧义的,而Lambda表达式中的 this 和表达式外部 this 予以相同

1
2
3
4
5
6
7
8
9
public class Hello {
Runnable r1 = () -> { System.out.println(this);};
Runnable r2 = () -> { System.out.println(toString()); };
public String toString() { return "Hello, world"; }
public static void main(String... args) {
new Hello().r1.run();
new Hello().r2.run();
}
}

变量捕获

在Java SE 7中,编译器对内部类中引用的外部变量(即捕获的变量)要求非常严格:如果捕获的变量没有被声明为final就会产生一个编译错误。对于lambda表达式和内部类,允许在其中捕获那些符合有效只读(Effectively final)的局部变量,尽管放宽了对捕获变量的语法限制,但试图修改捕获变量的行为仍然会被禁止。

简单的说在内部类中捕获的外部变量必须是final修饰的变量,儿Lambda表达式内捕获外部变量时不要求变量的final属性,但是依然不能修改。

栗子:

1
2
3
4
Callable<String> helloCallable(String name) {
String hello = "Hello";
return () -> (hello + ", " + name);
}

方法引用

方法引用和lambda表达式拥有相同的特性,不过我们并不需要为方法引用提供方法体,我们可以直接通过方法名称引用已有方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Person {
private final String name;
private final int age;
public int getAge() { return age; }
public String getName() {return name; }
...
}

Person[] people = ...
Comparator<Person> byName = Comparator.comparing(Person::getName);
Arrays.sort(people, byName);

Consumer<Integer> b1 = System::exit; // void exit(int status)
Consumer<String[]> b2 = Arrays:sort; // void sort(Object[] a)
Consumer<String> b3 = MyProgram::main; // void main(String... args)
Runnable r = Myprogram::mapToInt // void main(String... args)

方法引用的种类

方法引用有很多种,它们的语法如下:

  • 静态方法引用:ClassName::methodName
  • 实例上的实例方法引用:instanceReference::methodName
  • 超类上的实例方法引用:super::methodName
  • 类型上的实例方法引用:ClassName::methodName
  • 构造方法引用:Class::new
  • 数组构造方法引用:TypeName[]::new