Java 8:一文掌握 Lambda 表達式

本文將介紹 Java 8 新增的 Lambda 表達式,包括 Lambda 表達式的常見用法以及方法引用的用法,並對 Lambda 表達式的原理進行分析,最後對 Lambda 表達式的優缺點進行一個總結。

1. 概述

Java 8 引入的 Lambda 表達式的主要作用就是簡化部分的寫法。

能夠使用 Lambda 表達式的一個重要依據是必須有相應的函數接口。所謂函數接口,是指內部有且僅有一個抽象方法的接口。

Lambda 表達式的另一個依據是類型推斷機制。在上下文信息足夠的情況下,編譯器可以推斷出參數表的類型,而不需要顯式指名。

2. 常見用法

2.1 無參函數的簡寫

無參函數就是沒有參數的函數,例如 Runnable 接口的 run() 方法,其定義如下:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

在 Java 7 及之前版本,我們一般可以這樣使用:

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
        System.out.println("Jimmy");
    }
}).start();

從 Java 8 開始,無參函數的匿名內部類可以簡寫成如下方式:

() -> {
    執行語句
}

這樣接口名和函數名就可以省掉了。那麼,上面的示例可以簡寫成:

new Thread(() -> {
    System.out.println("Hello");
    System.out.println("Jimmy");
}).start();

當只有一條語句時,我們還可以對代碼塊進行簡寫,格式如下:

() -> 表達式

注意這裏使用的是表達式,並不是語句,也就是說不需要在末尾加分號。

那麼,當上面的例子中執行的語句只有一條時,可以簡寫成這樣:

new Thread(() -> System.out.println("Hello")).start();

2.2 單參函數的簡寫

單參函數是指只有一個參數的函數。例如 View 內部的接口 OnClickListener 的方法 onClick(View v),其定義如下:

public interface OnClickListener {
    /**
     * Called when a view has been clicked.
     *
     * @param v The view that was clicked.
     */
    void onClick(View v);
}

在 Java 7 及之前的版本,我們通常可能會這麼使用:

view.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        v.setVisibility(View.GONE);
    }
});

從 Java 8 開始,單參函數的匿名內部類可以簡寫成如下方式:

([類名 ]變量名) -> {
    執行語句
}


其中類名是可以省略的,因爲 Lambda 表達式可以自己推斷出來。那麼上面的例子可以簡寫成如下兩種方式:

view.setOnClickListener((View v) -> {
    v.setVisibility(View.GONE);
});
view.setOnClickListener((v) -> {
    v.setVisibility(View.GONE);
});

單參函數甚至可以把括號去掉,官方也更建議使用這種方式:

變量名 -> {
    執行語句
}

那麼,上面的示例可以簡寫成:

view.setOnClickListener(v -> {
    v.setVisibility(View.GONE);
});

當只有一條語句時,依然可以對代碼塊進行簡寫,格式如下:

([類名 ]變量名) -> 表達式

類名和括號依然可以省略,如下:

變量名 -> 表達式

那麼,上面的示例可以進一步簡寫成:

view.setOnClickListener(v -> v.setVisibility(View.GONE));

2.3 多參函數的簡寫

多參函數是指具有兩個及以上參數的函數。例如,Comparator 接口的 compare(T o1, T o2) 方法就具有兩個參數,其定義如下:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

在 Java 7 及之前的版本,當我們對一個集合進行排序時,通常可以這麼寫:

List<Integer> list = Arrays.asList(1, 2, 3);
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
});

從 Java 8 開始,多參函數的匿名內部類可以簡寫成如下方式:

([類名1 ]變量名1, [類名2 ]變量名2[, ...]) -> {
    執行語句
}

同樣類名可以省略,那麼上面的例子可以簡寫成:

Collections.sort(list, (Integer o1, Integer o2) -> {
    return o1.compareTo(o2);
});
Collections.sort(list, (o1, o2) -> {
    return o1.compareTo(o2);
});

當只有一條語句時,依然可以對代碼塊進行簡寫,格式如下:

([類名1 ]變量名1, [類名2 ]變量名2[, ...]) -> 表達式

此時類名也是可以省略的,但括號不能省略。如果這條語句需要返回值,那麼 return 關鍵字是不需要寫的。

因此,上面的示例可以進一步簡寫成:

Collections.sort(list, (o1, o2) -> o1.compareTo(o2));

最後呢,這個示例還可以簡寫成這樣:

Collections.sort(list, Integer::compareTo);

咦,這是什麼特性?這就是我們下面要講的內容:方法引用。

3. 方法引用

方法引用也是一個語法糖,可以用來簡化開發。

在我們使用 Lambda 表達式的時候,如果 “->” 的右邊要執行的表達式只是調用一個類已有的方法,那麼就可以用「方法引用」來替代 Lambda 表達式。

  • 引用靜態方法;
  • 引用對象的方法;
  • 引用類的方法;
  • 引用構造方法。

下面按照這 4 類分別進行闡述。

3.1 引用靜態方法

當我們要執行的表達式是調用某個類的靜態方法,並且這個靜態方法的參數列表和接口裏抽象函數的參數列表一一對應時,我們可以採用引用靜態方法的格式。

假如 Lambda 表達式符合如下格式:

([變量1, 變量2, ...]) -> 類名.靜態方法名([變量1, 變量2, ...])

我們可以簡寫成如下格式:

類名::靜態方法名

注意這裏靜態方法名後面不需要加括號,也不用加參數,因爲編譯器都可以推斷出來。下面我們繼續使用 2.3 節的示例來進行說明。

首先創建一個工具類,代碼如下:

public class Utils {
    public static int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

注意這裏的 compare() 函數的參數和 Comparable 接口的 compare() 函數的參數是一一對應的。然後一般的 Lambda 表達式可以這樣寫:

Collections.sort(list, (o1, o2) -> Utils.compare(o1, o2));

如果採用方法引用的方式,可以簡寫成這樣:

Collections.sort(list, Utils::compare);

3.2 引用對象的方法

當我們要執行的表達式是調用某個對象的方法,並且這個方法的參數列表和接口裏抽象函數的參數列表一一對應時,我們就可以採用引用對象的方法的格式。

假如 Lambda 表達式符合如下格式:

([變量1, 變量2, ...]) -> 對象引用.方法名([變量1, 變量2, ...])

我們可以簡寫成如下格式:

對象引用::方法名

下面我們繼續使用 2.3 節的示例來進行說明。首先創建一個類,代碼如下:

public class MyClass {
    public int compare(Integer o1, Integer o2) {
        return o1.compareTo(o2);
    }
}

當我們創建一個該類的對象,並在 Lambda 表達式中使用該對象的方法時,一般可以這麼寫:

MyClass myClass = new MyClass();
Collections.sort(list, (o1, o2) -> myClass.compare(o1, o2));

注意這裏函數的參數也是一一對應的,那麼採用方法引用的方式,可以這樣簡寫:

MyClass myClass = new MyClass();
Collections.sort(list, myClass::compare);

此外,當我們要執行的表達式是調用 Lambda 表達式所在的類的方法時,我們還可以採用如下格式:

this::方法名

例如我在 Lambda 表達式所在的類添加如下方法:

private int compare(Integer o1, Integer o2) {
    return o1.compareTo(o2);
}

當 Lambda 表達式使用這個方法時,一般可以這樣寫:

Collections.sort(list, (o1, o2) -> compare(o1, o2));

如果採用方法引用的方式,就可以簡寫成這樣:

Collections.sort(list, this::compare);

3.3 引用類的方法

引用類的方法所採用的參數對應形式與上兩種略有不同。如果 Lambda 表達式的 “->” 的右邊要執行的表達式是調用的 “->” 的左邊第一個參數的某個實例方法,並且從第二個參數開始(或無參)對應到該實例方法的參數列表時,就可以使用這種方法。

可能有點繞,假如我們的 Lambda 表達式符合如下格式:

(變量1[, 變量2, ...]) -> 變量1.實例方法([變量2, ...])

那麼我們的代碼就可以簡寫成:

變量1對應的類名::實例方法名

還是使用 2.3 節的例子, 當我們使用的 Lambda 表達式是這樣時:

Collections.sort(list, (o1, o2) -> o1.compareTo(o2));

按照上面的說法,就可以簡寫成這樣:

Collections.sort(list, Integer::compareTo);

3.4 引用構造方法

當我們要執行的表達式是新建一個對象,並且這個對象的構造方法的參數列表和接口裏函數的參數列表一一對應時,我們就可以採用「引用構造方法」的格式。

假如我們的 Lambda 表達式符合如下格式:

1
([變量1, 變量2, ...]) -> new 類名([變量1, 變量2, ...])


我們就可以簡寫成如下格式:


類名::new


下面舉個例子說明一下。Java 8 引入了一個 Function 接口,它是一個函數接口,部分代碼如下:

@FunctionalInterface
public interface Function<T, R> {
    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
		// 省略部分代碼
}


我們用這個接口來實現一個功能,創建一個指定大小的 ArrayList。一般我們可以這樣實現:

Function<Integer, ArrayList> function = new Function<Integer, ArrayList>() {
    @Override
    public ArrayList apply(Integer n) {
        return new ArrayList(n);
    }
};
List list = function.apply(10);

使用 Lambda 表達式,我們一般可以這樣寫:

Function<Integer, ArrayList> function = n -> new ArrayList(n);

使用「引用構造方法」的方式,我們可以簡寫成這樣:

Function<Integer, ArrayList> function = ArrayList::new;

4. 自定義函數接口

自定義函數接口很容易,只需要編寫一個只有一個抽象方法的接口即可,示例代碼:

@FunctionalInterface
public interface MyInterface<T> {
    void function(T t);
}


上面代碼中的 @FunctionalInterface 是可選的,但加上該註解編譯器會幫你檢查接口是否符合函數接口規範。就像加入 @Override 註解會檢查是否重寫了函數一樣。

5. 實現原理

經過上面的介紹,我們看到 Lambda 表達式只是爲了簡化匿名內部類書寫,看起來似乎在編譯階段把所有的 Lambda 表達式替換成匿名內部類就可以了。但實際情況並非如此,在 JVM 層面,Lambda 表達式和匿名內部類其實有着明顯的差別。

5.1 匿名內部類的實現

匿名內部類仍然是一個類,只是不需要我們顯式指定類名,編譯器會自動爲該類取名。比如有如下形式的代碼:

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World");
            }
        }).start();
    }
}

編譯之後將會產生兩個 class 文件:

LambdaTest.class
LambdaTest$1.class

使用 javap -c LambdaTest.class 進一步分析 LambdaTest.class 的字節碼,部分結果如下:

public static void main(java.lang.String[]);
  Code:
     0: new           #2                  // class java/lang/Thread
     3: dup
     4: new           #3                  // class com/example/myapplication/lambda/LambdaTest$1
     7: dup
     8: invokespecial #4                  // Method com/example/myapplication/lambda/LambdaTest$1."<init>":()V
    11: invokespecial #5                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
    14: invokevirtual #6                  // Method java/lang/Thread.start:()V
    17: return

可以發現在 4: new #3 這一行創建了匿名內部類的對象。

5.2 Lambda 表達式的實現

接下來我們將上面的示例代碼使用 Lambda 表達式實現,代碼如下:

public class LambdaTest {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello World")).start();
    }
}

此時編譯後只會產生一個文件 LambdaTest.class,再來看看通過 javap 對該文件反編譯後的結果:

public static void main(java.lang.String[]);
  Code:
     0: new           #2                  // class java/lang/Thread
     3: dup
     4: invokedynamic #3,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;
     9: invokespecial #4                  // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
    12: invokevirtual #5                  // Method java/lang/Thread.start:()V
    15: return

從上面的結果我們發現 Lambda 表達式被封裝成了主類的一個私有方法,並通過 invokedynamic 指令進行調用。

因此,我們可以得出結論:Lambda 表達式是通過 invokedynamic 指令實現的,並且書寫 Lambda 表達式不會產生新的類。

既然 Lambda 表達式不會創建匿名內部類,那麼在 Lambda 表達式中使用 this 關鍵字時,其指向的是外部類的引用。

6. 優缺點

優點:

  • 可以減少代碼的書寫,減少匿名內部類的創建,節省內存佔用。
  • 使用時不用去記憶所使用的接口和抽象函數。

缺點:

  • 易讀性較差,閱讀代碼的人需要熟悉 Lambda 表達式和抽象函數中參數的類型。
  • 不方便進行調試。

參考


“不積跬步,無以至千里”,希望未來的你能:有夢爲馬 隨處可棲!加油,少年!

關注公衆號:「Java 知己」,每天更新Java知識哦,期待你的到來!

  • 發送「Group」,與 10 萬程序員一起進步。
  • 發送「面試」,領取BATJ面試資料、面試視頻攻略。
  • 發送「玩轉算法」,領取《玩轉算法》系列視頻教程。
  • 千萬不要發送「1024」…

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章