Java 8:一文掌握 Lambda 表達(dá)式

本文將介紹 Java 8 新增的 Lambda 表達(dá)式,包括 Lambda 表達(dá)式的常見(jiàn)用法以及方法引用的用法,并對(duì) Lambda 表達(dá)式的原理進(jìn)行分析,最后對(duì) Lambda 表達(dá)式的優(yōu)缺點(diǎn)進(jìn)行一個(gè)總結(jié)。

目錄

1. 概述

Java 8 引入的 Lambda 表達(dá)式的主要作用就是簡(jiǎn)化部分匿名內(nèi)部類的寫(xiě)法。

能夠使用 Lambda 表達(dá)式的一個(gè)重要依據(jù)是必須有相應(yīng)的函數(shù)接口。所謂函數(shù)接口,是指內(nèi)部有且僅有一個(gè)抽象方法的接口。

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

2. 常見(jiàn)用法

2.1 無(wú)參函數(shù)的簡(jiǎn)寫(xiě)

無(wú)參函數(shù)就是沒(méi)有參數(shù)的函數(shù),例如 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 開(kāi)始,無(wú)參函數(shù)的匿名內(nèi)部類可以簡(jiǎn)寫(xiě)成如下方式:

() -> {
    執(zhí)行語(yǔ)句
}

這樣接口名和函數(shù)名就可以省掉了。那么,上面的示例可以簡(jiǎn)寫(xiě)成:

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

當(dāng)只有一條語(yǔ)句時(shí),我們還可以對(duì)代碼塊進(jìn)行簡(jiǎn)寫(xiě),格式如下:

() -> 表達(dá)式

注意這里使用的是表達(dá)式,并不是語(yǔ)句,也就是說(shuō)不需要在末尾加分號(hào)。

那么,當(dāng)上面的例子中執(zhí)行的語(yǔ)句只有一條時(shí),可以簡(jiǎn)寫(xiě)成這樣:

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

2.2 單參函數(shù)的簡(jiǎn)寫(xiě)

單參函數(shù)是指只有一個(gè)參數(shù)的函數(shù)。例如 View 內(nèi)部的接口 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 及之前的版本,我們通??赡軙?huì)這么使用:

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

從 Java 8 開(kāi)始,單參函數(shù)的匿名內(nèi)部類可以簡(jiǎn)寫(xiě)成如下方式:

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

其中類名是可以省略的,因?yàn)?Lambda 表達(dá)式可以自己推斷出來(lái)。那么上面的例子可以簡(jiǎn)寫(xiě)成如下兩種方式:

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

單參函數(shù)甚至可以把括號(hào)去掉,官方也更建議使用這種方式:

變量名 -> {
    執(zhí)行語(yǔ)句
}

那么,上面的示例可以簡(jiǎn)寫(xiě)成:

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

當(dāng)只有一條語(yǔ)句時(shí),依然可以對(duì)代碼塊進(jìn)行簡(jiǎn)寫(xiě),格式如下:

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

類名和括號(hào)依然可以省略,如下:

變量名 -> 表達(dá)式

那么,上面的示例可以進(jìn)一步簡(jiǎn)寫(xiě)成:

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

2.3 多參函數(shù)的簡(jiǎn)寫(xiě)

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

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

在 Java 7 及之前的版本,當(dāng)我們對(duì)一個(gè)集合進(jìn)行排序時(shí),通常可以這么寫(xiě):

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 開(kāi)始,多參函數(shù)的匿名內(nèi)部類可以簡(jiǎn)寫(xiě)成如下方式:

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

同樣類名可以省略,那么上面的例子可以簡(jiǎn)寫(xiě)成:

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

當(dāng)只有一條語(yǔ)句時(shí),依然可以對(duì)代碼塊進(jìn)行簡(jiǎn)寫(xiě),格式如下:

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

此時(shí)類名也是可以省略的,但括號(hào)不能省略。如果這條語(yǔ)句需要返回值,那么 return 關(guān)鍵字是不需要寫(xiě)的。

因此,上面的示例可以進(jìn)一步簡(jiǎn)寫(xiě)成:

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

最后呢,這個(gè)示例還可以簡(jiǎn)寫(xiě)成這樣:

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

咦,這是什么特性?這就是我們下面要講的內(nèi)容:方法引用。

3. 方法引用

方法引用也是一個(gè)語(yǔ)法糖,可以用來(lái)簡(jiǎn)化開(kāi)發(fā)。

在我們使用 Lambda 表達(dá)式的時(shí)候,如果“->”的右邊要執(zhí)行的表達(dá)式只是調(diào)用一個(gè)類已有的方法,那么就可以用「方法引用」來(lái)替代 Lambda 表達(dá)式。

方法引用可以分為 4 類:

  • 引用靜態(tài)方法;
  • 引用對(duì)象的方法;
  • 引用類的方法;
  • 引用構(gòu)造方法。

下面按照這 4 類分別進(jìn)行闡述。

3.1 引用靜態(tài)方法

當(dāng)我們要執(zhí)行的表達(dá)式是調(diào)用某個(gè)類的靜態(tài)方法,并且這個(gè)靜態(tài)方法的參數(shù)列表和接口里抽象函數(shù)的參數(shù)列表一一對(duì)應(yīng)時(shí),我們可以采用引用靜態(tài)方法的格式。

假如 Lambda 表達(dá)式符合如下格式:

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

我們可以簡(jiǎn)寫(xiě)成如下格式:

類名::靜態(tài)方法名

注意這里靜態(tài)方法名后面不需要加括號(hào),也不用加參數(shù),因?yàn)榫幾g器都可以推斷出來(lái)。下面我們繼續(xù)使用 2.3 節(jié)的示例來(lái)進(jìn)行說(shuō)明。

首先創(chuàng)建一個(gè)工具類,代碼如下:

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

注意這里的 compare() 函數(shù)的參數(shù)和 Comparable 接口的 compare() 函數(shù)的參數(shù)是一一對(duì)應(yīng)的。然后一般的 Lambda 表達(dá)式可以這樣寫(xiě):

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

如果采用方法引用的方式,可以簡(jiǎn)寫(xiě)成這樣:

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

3.2 引用對(duì)象的方法

當(dāng)我們要執(zhí)行的表達(dá)式是調(diào)用某個(gè)對(duì)象的方法,并且這個(gè)方法的參數(shù)列表和接口里抽象函數(shù)的參數(shù)列表一一對(duì)應(yīng)時(shí),我們就可以采用引用對(duì)象的方法的格式。

假如 Lambda 表達(dá)式符合如下格式:

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

我們可以簡(jiǎn)寫(xiě)成如下格式:

對(duì)象引用::方法名

下面我們繼續(xù)使用 2.3 節(jié)的示例來(lái)進(jìn)行說(shuō)明。首先創(chuàng)建一個(gè)類,代碼如下:

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

當(dāng)我們創(chuàng)建一個(gè)該類的對(duì)象,并在 Lambda 表達(dá)式中使用該對(duì)象的方法時(shí),一般可以這么寫(xiě):

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

注意這里函數(shù)的參數(shù)也是一一對(duì)應(yīng)的,那么采用方法引用的方式,可以這樣簡(jiǎn)寫(xiě):

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

此外,當(dāng)我們要執(zhí)行的表達(dá)式是調(diào)用 Lambda 表達(dá)式所在的類的方法時(shí),我們還可以采用如下格式:

this::方法名

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

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

當(dāng) Lambda 表達(dá)式使用這個(gè)方法時(shí),一般可以這樣寫(xiě):

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

如果采用方法引用的方式,就可以簡(jiǎn)寫(xiě)成這樣:

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

3.3 引用類的方法

引用類的方法所采用的參數(shù)對(duì)應(yīng)形式與上兩種略有不同。如果 Lambda 表達(dá)式的“->”的右邊要執(zhí)行的表達(dá)式是調(diào)用的“->”的左邊第一個(gè)參數(shù)的某個(gè)實(shí)例方法,并且從第二個(gè)參數(shù)開(kāi)始(或無(wú)參)對(duì)應(yīng)到該實(shí)例方法的參數(shù)列表時(shí),就可以使用這種方法。

可能有點(diǎn)繞,假如我們的 Lambda 表達(dá)式符合如下格式:

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

那么我們的代碼就可以簡(jiǎn)寫(xiě)成:

變量1對(duì)應(yīng)的類名::實(shí)例方法名

還是使用 2.3 節(jié)的例子, 當(dāng)我們使用的 Lambda 表達(dá)式是這樣時(shí):

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

按照上面的說(shuō)法,就可以簡(jiǎn)寫(xiě)成這樣:

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

3.4 引用構(gòu)造方法

當(dāng)我們要執(zhí)行的表達(dá)式是新建一個(gè)對(duì)象,并且這個(gè)對(duì)象的構(gòu)造方法的參數(shù)列表和接口里函數(shù)的參數(shù)列表一一對(duì)應(yīng)時(shí),我們就可以采用「引用構(gòu)造方法」的格式。

假如我們的 Lambda 表達(dá)式符合如下格式:

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

我們就可以簡(jiǎn)寫(xiě)成如下格式:

類名::new

下面舉個(gè)例子說(shuō)明一下。Java 8 引入了一個(gè) Function 接口,它是一個(gè)函數(shù)接口,部分代碼如下:

@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);
        // 省略部分代碼
}

我們用這個(gè)接口來(lái)實(shí)現(xiàn)一個(gè)功能,創(chuàng)建一個(gè)指定大小的 ArrayList。一般我們可以這樣實(shí)現(xiàn):

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

使用 Lambda 表達(dá)式,我們一般可以這樣寫(xiě):

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

使用「引用構(gòu)造方法」的方式,我們可以簡(jiǎn)寫(xiě)成這樣:

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

4. 自定義函數(shù)接口

自定義函數(shù)接口很容易,只需要編寫(xiě)一個(gè)只有一個(gè)抽象方法的接口即可,示例代碼:

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

上面代碼中的 @FunctionalInterface 是可選的,但加上該注解編譯器會(huì)幫你檢查接口是否符合函數(shù)接口規(guī)范。就像加入 @Override 注解會(huì)檢查是否重寫(xiě)了函數(shù)一樣。

5. 實(shí)現(xiàn)原理

經(jīng)過(guò)上面的介紹,我們看到 Lambda 表達(dá)式只是為了簡(jiǎn)化匿名內(nèi)部類書(shū)寫(xiě),看起來(lái)似乎在編譯階段把所有的 Lambda 表達(dá)式替換成匿名內(nèi)部類就可以了。但實(shí)際情況并非如此,在 JVM 層面,Lambda 表達(dá)式和匿名內(nèi)部類其實(shí)有著明顯的差別。

5.1 匿名內(nèi)部類的實(shí)現(xiàn)

匿名內(nèi)部類仍然是一個(gè)類,只是不需要我們顯式指定類名,編譯器會(huì)自動(dòng)為該類取名。比如有如下形式的代碼:

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

編譯之后將會(huì)產(chǎn)生兩個(gè) class 文件:

LambdaTest.class
LambdaTest$1.class

使用 javap -c LambdaTest.class 進(jìn)一步分析 LambdaTest.class 的字節(jié)碼,部分結(jié)果如下:

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

可以發(fā)現(xiàn)在 4: new #3 這一行創(chuàng)建了匿名內(nèi)部類的對(duì)象。

5.2 Lambda 表達(dá)式的實(shí)現(xiàn)

接下來(lái)我們將上面的示例代碼使用 Lambda 表達(dá)式實(shí)現(xiàn),代碼如下:

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

此時(shí)編譯后只會(huì)產(chǎn)生一個(gè)文件 LambdaTest.class,再來(lái)看看通過(guò) javap 對(duì)該文件反編譯后的結(jié)果:

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

從上面的結(jié)果我們發(fā)現(xiàn) Lambda 表達(dá)式被封裝成了主類的一個(gè)私有方法,并通過(guò) invokedynamic 指令進(jìn)行調(diào)用。

因此,我們可以得出結(jié)論:Lambda 表達(dá)式是通過(guò) invokedynamic 指令實(shí)現(xiàn)的,并且書(shū)寫(xiě) Lambda 表達(dá)式不會(huì)產(chǎn)生新的類。

既然 Lambda 表達(dá)式不會(huì)創(chuàng)建匿名內(nèi)部類,那么在 Lambda 表達(dá)式中使用 this 關(guān)鍵字時(shí),其指向的是外部類的引用。

6. 優(yōu)缺點(diǎn)

優(yōu)點(diǎn):

  • 可以減少代碼的書(shū)寫(xiě),減少匿名內(nèi)部類的創(chuàng)建,節(jié)省內(nèi)存占用。
  • 使用時(shí)不用去記憶所使用的接口和抽象函數(shù)。

缺點(diǎn):

  • 易讀性較差,閱讀代碼的人需要熟悉 Lambda 表達(dá)式和抽象函數(shù)中參數(shù)的類型。
  • 不方便進(jìn)行調(diào)試。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容