Java 8 - Lambda表達式

本篇來自於 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();
  }
}

與之相類似的內部類實現則會打印出類似Hello1@5b89a773Hello 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));
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章