本篇来自于 State of Lambda by Brian Goetz
Java 8 包括的首要的语言新特性有:
- Lambda 表达式 (非正式式的称为”闭包”或”匿名方法”)
- 方法和构造器引用
- 扩展的目标类型和类型推导
- 接口中缺省的(default) 和静态的(static)方法
一. 背景。
Java面向对象编程,有些对象仅仅只有一个方法,典型的情况是 一个Java API定义了一个接口 (所谓 回调接口), 用户通过匿名实例化这个接口(匿名内部类),然后调用这个API。 例如:
public interface Runnable{ //functional interface
public void run();
}
public class Foo {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
public void run() {
System.out.println("new thread");
}
};
new Thread(runnable).start();
}
}
匿名内部类有些缺点, 首先:
- 代码冗余
- 内部类的this和变量名容易误解
- 类型载入和实例创建语义不够灵活
- 无法访问非final的本地变量
- 无法抽象控制流
考虑到对函数式编程的支持,Java 8增加了Lambda表达式的语言新特性,这样可以简化 实现只有一个方法的匿名内部类的代码。
二. 函数接口(Function Interface)。
Java 8 以前的版本,接口(interface)中只能定义没有实现的方法(没有方法体 body)。这有很大的弊端,一旦接口改变增加一个新的方法,所有实现此接口的类都要改变。为此Java 8 接口支持 缺省接口方法(修饰符default) 和 静态接口方法(修饰符static),这两种方法都有实现的body。
如果接口中只有一个没有实现的方法 如 Runnable, 这种类型的接口称为 函数接口。(如果我们定义一个函数接口,可以添加注解FunctionalInterface让编译器来检验是否是函数接口)。
例如 Java 7 定义的函数接口有:
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.beans.PropertyChangeListener
另外,Java 8 添加了个新的包,java.util.function, 包含一些常用的函数接口,如:
Predicate<T> -- a boolean-valued property of an object
Consumer<T> -- an action to be performed on an object
Function<T,R> -- a function transforming a T to a R
Supplier<T> -- provide an instance of a T (such as a factory)
UnaryOperator<T> -- a function from T to T
BinaryOperator<T> -- a function from (T, T) to T
其他常用的函数接口 如:
java.io.FileFilter
三. Lambda 表达式
匿名内部类最大的痛点是笨重,如前面的Runnable实例有5行代码。
Lambda表达式是匿名方法,针对函数接口,目的以一个轻量级的机制取代内部类的机制。
Lambda表达式的例子如:
(int x, int y) -> x + y
() -> 42
(String s) -> { System.out.println(s); }
第一个表达式 带两个参数,返回它们的总数;
第二个表达式 未带参数,返回证书 42;
第三个表达式 带一个String参数,将这个参数打印到控制台,不返回。
Lambda表达式由三部分组成:
- 一个参数列表
- 一个箭头 ->
- 一个方法体
前面的代码用lambda表达式可以写成:
public class Foo {
public static void main(String[] args) {
new Thread(()->System.out.println("new thread")).start();
}
}
使用lambda表达式,代码很紧凑,语义清晰。
四. 目标类型
一个函数接口不是lambda表达式语法的一部分,一个lambda表达式代表什么类型的对象呢?它的类型根据上下文推导(type inference)。
如上面的代码,Thread构造器的参数类型是Runnable, 所以Lambda表达式
System.out.println(“new thread”) 代表一个Runnable实例。
run()方法匿名;没有返回值;方法体是 System.out.println(“new thread”)。
编译器负责推导每个lambda表达式的类型,称之为目标类型(Target Type)。
一个lambda表达式只能出现在一个目标类型是一个函数接口的上下文。
也就是, 一个lambda表达式能被赋值于一个目标类型T应该满足下面条件:
- T 是一个函数接口类型
- Lambda 表达式的参数和T的参数 (数量和类型)相同
- Lambda 表达式的body返回的类型与T的方法返回类型兼容
- Lambda 表达式的body抛出的异常能被T的方法抛出
因为参数类型可以推导出,所有可以省略, 如下面的 参数s1和s2的类型可以推导出是String, 所以省略。
Comparator<String> c = (s1, s2) -> s1.compareToIgnoreCase(s2);
如果只有一个参数,可以省略包围参数的括号。如下面的lambda表达式, (f) 省略成 f。
FileFilter java = f -> f.getName().endsWith(".java");
五. 目标类型的上下文
下面的上下文有目标类型
- 变量声明
- 返回声明
- 数组的初始化
- 方法的参数
- Lambda 表达式的bodies
- 条件表达式 (?:)
- 造型表达式 (Cast)
Comparator<String> c;
c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);
public Runnable toDoLater() {
return () -> {
System.out.println("later");
};
}
Supplier<Runnable> c = () -> () -> { System.out.println("hi"); };
Callable<Integer> c = flag ? (() -> 23) : (() -> 42);
Object o = (Runnable) () -> { System.out.println("hi"); };
目标类型不限于lambda 表达式, Java 8支持泛型类型的目标类型。
List<String> ls =
Collections.checkedList(new ArrayList<>(), String.class);
Set<Integer> si = flag ? Collections.singleton(23)
: Collections.emptySet();
六. 词法作用域
在内部类中使用变量名(以及this)非常容易出错。内部类中通过继承得到的成员(包括来自Object的方法)可能会把外部类的成员掩盖(shadow),此外未限定(unqualified)的this引用会指向内部类自己而非外部类。
相对于内部类,lambda表达式的语义就十分简单:它不会从超类(supertype)中继承任何变量名,也不会引入一个新的作用域。lambda表达式基于词法作用域,也就是说lambda表达式函数体里面的变量和它外部环境的变量具有相同的语义(也包括lambda表达式的形式参数)。此外,’this’关键字及其引用在lambda表达式内部和外部也拥有相同的语义。
为了进一步说明词法作用域的优点,请参考下面的代码,它会把”Hello, world!”打印两遍:
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();
}
}
与之相类似的内部类实现则会打印出类似Hello 1@5b89a773和Hello 2@537a7706之类的字符串,这往往会使开发者大吃一惊。
基于词法作用域的理念,lambda表达式不可以掩盖任何其所在上下文中的局部变量,它的行为和那些拥有参数的控制流结构(例如for循环和catch从句)一致。
七. 变量捕获
内部类实例会一直保留一个对其外部类实例的强引用,而那些没有捕获外部类成员的lambda表达式则不会保留对外部类实例的引用。内部类的这个特性往往会造成内存泄露。
Callable<String> helloCallable(String name) {
String hello = "Hello";
return () -> (hello + ", " + name);
}
Lambda表达式禁止可变的本地变量,如:
int sum = 0;
list.forEach(e -> { sum += e.size(); }); // ERROR
Lambda表达式对值封闭,对变量开放。
List<Person> list = new ArrayList<>();
list.forEach((x) ->x.setName("Tom")); //OK
八. 方法引用
方法引用包括:
- 静态方法引用 (ClassName::methName)
- 实例方法引用 (instanceRef::methName)
- 超类方法引用 (super::methName)
- 特殊类型的实例方法引用 (ClassName::methName)
- 类构造器引用(ClassName::new)
- 数组构造器引用 (TypeName[]::new)
方法前有个分隔符::
例如:
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(p -> p.getName());
Arrays.sort(people, byName);
如果将p.getName()的换成方法引用,上面的代码可以重写为:
Comparator<Person> byName = Comparator.comparing(Person::getName);
其他的例子:
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::main; // void main(String... args)
Consumer consumer = System.out::println;
等价于Consumer consumer = x->System.out.println(x);
八. 缺省方法和静态方法
接口中可以定义缺省方法和静态方法。
默认方法拥有其默认实现,实现接口的类型通过继承得到该默认实现(如果类型没有覆盖该默认实现)。此外,默认方法不是抽象方法,所以我们可以放心的向函数式接口里增加默认方法,而不用担心函数式接口的单抽象方法限制。
九. 汇总
Java 8新语言特性—lambda表达式,方法引用,默认方法和静态接口方法,以及范围更广的类型推导。开发者可以使用它们编写出更加清晰简洁的代码,类库编写者可以编写更加强大易用的并行类库。
例如:下面的代码太冗余.
List<Person> people = ...
Collections.sort(people, new Comparator<Person>() {
public int compare(Person x, Person y) {
return x.getLastName().compareTo(y.getLastName());
}
})
使用lambda表达式和类库,可以写成如下:
Collections.sort(people,
(Person x, Person y) -> x.getLastName().compareTo(y.getLastName()));
或者
Collections.sort(people, Comparator.comparing(Person::getName));
或者
list.sort((Person x, Person y) -> x.getName().compareTo(y.getName()));
或者
list.sort(Comparator.comparing(Person::getName));