本篇來自於 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
基於詞法作用域的理念,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));