Java8 新特性(一) - Lambda 表達(dá)式

Java8 新特性(一) - Lambda 表達(dá)式

近些日子一直在使用和研究 golang,很長(zhǎng)時(shí)間沒(méi)有關(guān)心 java 相關(guān)的知識(shí),前些天看到 java9 已經(jīng)正式發(fā)布,意識(shí)到自己的 java 知識(shí)已經(jīng)落后很多,心里莫名焦慮,決定將拉下的知識(shí)補(bǔ)上。

Lambda 表達(dá)式的淵源

Java8 作為近年來(lái)最重要的更新之一,為開(kāi)發(fā)者帶來(lái)了很多新特性,可能在很多其他語(yǔ)言中早已實(shí)現(xiàn),但來(lái)的晚總比不來(lái)好。Lambda 表達(dá)式就是 Java8 帶來(lái)的最重要的特性之一。

Lambda 表達(dá)式為 Java8 帶來(lái)了部分函數(shù)式編程的支持。Lambda 表達(dá)式雖然不完全等同于閉包,但也基本實(shí)現(xiàn)了閉包的功能。和其他一些函數(shù)式語(yǔ)言不一樣的是,Java 中的 Lambda 表達(dá)式也是對(duì)象,必須依附于一類特別的對(duì)象類型,函數(shù)式接口。

為什么需要 Lambda 表達(dá)式

內(nèi)循環(huán) VS. 外循環(huán)

先看一個(gè)非常簡(jiǎn)單的例子, 打印 list 內(nèi)所有元素:

        List<Interger> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9)

        for (int number: bumbers) {
            System.out.println(number)
        }

作為一個(gè) Java 開(kāi)發(fā)者,你這一生可能已經(jīng)寫過(guò)無(wú)數(shù)次類似代碼??瓷先ズ孟裢玫?,沒(méi)有什么需要改進(jìn)的,我們顯式的在外部迭代遍歷 list 內(nèi)元素,并挨個(gè)處理其中元素。那為什么提倡內(nèi)部迭代呢,因?yàn)閮?nèi)部迭代有助于 JIT 的優(yōu)化,JIT 可以將處理元素的過(guò)程并行化。

在 Java8 之前,需要借助 Guava 或其他第三方庫(kù)來(lái)實(shí)現(xiàn)內(nèi)部迭代,而在 Java8 中, 我們可以用以下代碼實(shí)現(xiàn):

        list.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });

以上代碼還是稍顯繁瑣,需要?jiǎng)?chuàng)建一個(gè)匿名類,使用 lambda 表達(dá)式后,可以大大簡(jiǎn)化代碼

        list.forEach((a) -> System.out.println(a));

Java 8 中 還引入了雙冒號(hào)運(yùn)算符,用于類方法引用,以上方法可以進(jìn)一步簡(jiǎn)化為

        list.forEach(System.out::println);

內(nèi)循環(huán)描述你要干什么,更符合自然語(yǔ)言描述的邏輯

passing behavior,not only value

通過(guò) lambda 表達(dá)式,我們可以在傳參時(shí),不僅可以將值傳入,還可將相關(guān)行為也傳入,這樣可以實(shí)現(xiàn)更加抽象和通用,更易復(fù)用的 API。看一下代碼例子,需要實(shí)現(xiàn)一個(gè)求 list 內(nèi)所有元素和的方法,嗯,看上去很簡(jiǎn)單。

public int sumAll(List<Integer> numbers) {
    int total = 0;
    for (int number : numbers) {
        total += number;
    }
    return total;
}

這個(gè)時(shí)候,又有需求實(shí)現(xiàn)一個(gè) list 內(nèi)所有偶數(shù)和的方法,簡(jiǎn)單,代碼復(fù)制一遍,稍作修改。

public int sumAllEven(List<Integer> numbers) {
    int total = 0;
    for (int number : numbers) {
        if (number % 2 == 0) {
            total += number;
        }
    }
    return total;
}

也沒(méi)發(fā)多少功夫,還需要改進(jìn)么,這個(gè)時(shí)候又需要所有奇數(shù)和呢,不同的需求過(guò)來(lái),你需要一遍又一遍的復(fù)制代碼。有沒(méi)有更加優(yōu)雅的解決方法呢?我們又想起了我們的 lambda 表達(dá)式,java 8 引入了一個(gè)新的函數(shù)接口 Predicate<T>, 使用它來(lái)定義 filter,代碼如下

public int sumAll(List<Integer> numbers, Predicate<Integer> p) {
    int total = 0;
    for (int number : numbers) {
        if (p.test(number)) {
            total += number;
        }
    }
    return total;
}

這樣以上兩個(gè)方法都可以通過(guò)這個(gè)方法實(shí)現(xiàn),并且可以非常容易的擴(kuò)展,當(dāng)你需要用其他條件實(shí)現(xiàn)元素篩選求和時(shí),只需要實(shí)現(xiàn)篩選條件的 lambda 表達(dá)式,如下

        System.out.println(sumAll(list, (a)-> true));           \\ 所有元素和
        System.out.println(sumAll(list, (a) -> a % 2 == 0));    \\ 所有偶數(shù)和
        System.out.println(sumAll(list, (a) -> a % 2 != 0));    \\ 所有奇數(shù)和

有同學(xué)會(huì)說(shuō),以前不用 lambda 表達(dá)式我們用接口也能實(shí)現(xiàn)。沒(méi)錯(cuò),用接口 + 匿名類也能實(shí)現(xiàn)類似效果,但 lambda 表達(dá)式更加直觀,代碼簡(jiǎn)捷,可讀性也強(qiáng),開(kāi)發(fā)者也更有動(dòng)力使用類似代碼。

利于寫出優(yōu)雅可讀性更高的代碼

先看一段代碼:

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
        
        for (int number : list) {
            if (number % 2 == 0) {
                int n2 = number * 2;
                if (n2 > 5) {
                    System.out.println(n2);
                    break;
                }
            }
        }

這個(gè)代碼也不難理解,取了 list 中的偶數(shù),乘以 2 后 大于 5 的第一個(gè)數(shù),這個(gè)代碼看上去不難,但是當(dāng)你在實(shí)際業(yè)務(wù)代碼中添加更多的邏輯時(shí),就會(huì)顯得可讀性較差。使用 Java 8 新加入的 stream api 和 lambda 表達(dá)式重構(gòu)這段代碼后,如下

        System.out.println(
                list.stream()
                        .filter((a) -> a % 2 == 0)
                        .map((b) -> b * 2)
                        .filter(c -> c > 5)
                        .findFirst()
        );

一行代碼就實(shí)現(xiàn)了以上功能,并且可讀性也好,從做至右依次讀過(guò)去,先篩選 偶數(shù),在乘以 2, 再篩選大于 5 的數(shù),取第一個(gè)數(shù)。并且 stream api 都是惰性的api,且不占用多余的空間,比如上面這段代碼,并不會(huì)把list 中所有元素都遍歷,當(dāng)找到第一個(gè)符合要求的元素后就會(huì)停止。

Lambda 表達(dá)式語(yǔ)法

Lambda 表達(dá)式的語(yǔ)法定義在 Java 8 規(guī)范 15.27 中,并給出了一些例子

() -> {}                    // 無(wú)參數(shù),body 為空
() -> 42                    // 無(wú)參數(shù),表達(dá)式的值作為返回
() -> {return 42;}          // 無(wú)參數(shù),block 塊
() -> {System.gc();}
() -> {
    if (true) return 23;
    else {
        return 14
    }
}
(int x) -> {return x + 1;}  // 有參數(shù),且顯式聲明參數(shù)類型
(int x) -> x + 1            
(x) -> x + 1                // 有參數(shù),未顯式聲明參數(shù)類型,編譯器推斷參數(shù)類型
x -> x + 1          
(int x, int y) -> x + y
(x, y) -> x + y         
(x, int y) -> x + y         // 非法, 參數(shù)類型顯示指定不能混用

總結(jié)一下:

  • Lambda 表達(dá)式可以具有零個(gè),一個(gè)或多個(gè)參數(shù)。
  • 可以顯式聲明參數(shù)的類型,也可以由編譯器自動(dòng)從上下文推斷參數(shù)類型。
  • 參數(shù)用小括號(hào)括起來(lái),用逗號(hào)分隔。例如 (a, b) 或 (int a, int b) 或 (String a, int b, float c)
  • 空括號(hào)用于表示一組空的參數(shù)。
  • 當(dāng)僅有一個(gè)參數(shù)時(shí),且不顯式指明類型,則可省略小括號(hào)
  • Lambda 表達(dá)式的正文可以包含零條,一條或多條語(yǔ)句。
  • 如果 Lambda 表達(dá)式的正文只有一條語(yǔ)句,則大括號(hào)可不用寫
  • 如果 Lambda 表達(dá)式的正文有一條以上的語(yǔ)句必須包含在代碼塊中

Functional Interface (函數(shù)接口)

還有一個(gè)問(wèn)題,在上面的內(nèi)容沒(méi)有提到,怎樣在聲明的時(shí)候表示 Lambda 表達(dá)式呢?比如函數(shù)可以接受一個(gè)Lambda表達(dá)式作為輸入。Java 8 引入了一種新的概念,叫函數(shù)接口。其實(shí)說(shuō)起來(lái)也不是什么新鮮東西,函數(shù)接口就是一種只包含一個(gè)抽象方法的接口(可以包含其他默認(rèn)方法),同時(shí) Java 8 引入一個(gè)新的注解 @FunctionalInterface,雖然不使用 FunctionalInterface 注解也可以使用,但是使用注解可以顯式的聲明該接口為函數(shù)接口,并且當(dāng)接口不符合函數(shù)接口要求時(shí),在編譯期間拋出錯(cuò)誤。之前 Java 已有的很多接口加上了該注解,最常見(jiàn)的比如 Runnable

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

也就是說(shuō),現(xiàn)在啟動(dòng)一個(gè)線程時(shí),可以采用新的 Lambda 表達(dá)式

new Thread(
    () -> System.out.println("hello world")
).start()

之前已經(jīng)存在的接口還有

java.lang.Comparable
java.util.concurrent.Callable

Java 8 中還新加了一些函數(shù)接口

java.util.function.Consumer<T>  // 消費(fèi)一個(gè)元素,無(wú)返回
java.util.function.Supplier<T>  // 每次返回一個(gè) T 類型的對(duì)象
java.util.function.Predicate<T> // 輸入一個(gè)元素,返回 boolean 值,常用于 filter
java.util.function.Function<T,R> // 輸入一個(gè) T 類型元素,返回一個(gè) R 類型對(duì)象

Lambda 表達(dá)式與匿名類

看上面的內(nèi)容,一定會(huì)有人認(rèn)為這些功能我使用匿名類也可以實(shí)現(xiàn),那 Lambda 表達(dá)式和匿名類有什么區(qū)別呢。最明顯的區(qū)別就是 this 指針,this 指針在匿名類中代表是匿名類,而在 Lambda 表達(dá)式中為包含 Lambda 表達(dá)式的類。同時(shí),匿名類可以實(shí)現(xiàn)多個(gè)方法,而 Lambda 表達(dá)式只能有一個(gè)方法。
直觀上,很多人會(huì)覺(jué)得 Lambda 表達(dá)式可能只是一個(gè)語(yǔ)法糖,最終轉(zhuǎn)換為一個(gè)匿名類。事實(shí)上,考慮到實(shí)現(xiàn)效率問(wèn)題,和向前兼容問(wèn)題,Java 8 并沒(méi)有采用匿名類語(yǔ)法糖,也沒(méi)有和其他語(yǔ)言一樣,采用專門的函數(shù)處理類型來(lái)實(shí)現(xiàn) lambda 表達(dá)式。

lambda 實(shí)現(xiàn)

既然 lambda 表達(dá)式并未用匿名類的方式實(shí)現(xiàn),那其原理到底是什么呢,之前我們分析泛型的時(shí)候都是分析字節(jié)碼,這里也一樣。我們先看一段代碼和字節(jié)碼。

public class LambdaStudy004 {
    public void print() {
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        list.forEach(x -> System.out.println(x));
    }
}

javap -p 結(jié)果

public class lambda.LambdaStudy004 {
  public lambda.LambdaStudy004();
  public void print();
  private static void lambda$print$0(java.lang.Integer);
}

很明顯,lambda 表達(dá)式編譯后,會(huì)生成類的一個(gè)私有靜態(tài)方法,然而,事情并沒(méi)有那么簡(jiǎn)單,雖然生成了一個(gè)靜態(tài)方法,lambda 表達(dá)式本身又由什么表示呢,java 中沒(méi)有函數(shù)指針,總要有一個(gè)類作為載體調(diào)用該靜態(tài)方法。

javap -p -v 查看字節(jié)碼

...

37: invokedynamic #5,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
42: invokeinterface #6,  2            // InterfaceMethod java/util/List.forEach:(Ljava/util/function/Consumer;)
47: return

...

和普通的 static 方法調(diào)用采用 invokestatic 指令不一樣,lambda 表達(dá)式的調(diào)用采用了 java 7 新引入的 invokedynamic 指令,該指令是為了加強(qiáng) java 的動(dòng)態(tài)語(yǔ)言特性引入,當(dāng) invokedynamic 指令被調(diào)用時(shí),會(huì)調(diào)用 metafactory 函數(shù)動(dòng)態(tài)生成一個(gè)實(shí)現(xiàn)了函數(shù)接口的對(duì)象,該對(duì)象實(shí)現(xiàn)的方法實(shí)際調(diào)用了之前生成的 static 方法,這個(gè)對(duì)象才是 lambda 表達(dá)式的實(shí)際翻譯后的表示,翻譯代碼如下

class LambdaStudy004Inner {
    private static void lambda$print$0(Integer x) {
        System.out.println(x);
    }

    private class lambda$1 implements Consumer<Integer> {
        @Override
        public void accept(Integer x) {
            LambdaStudy004Inner.lambda$print$0(x);
        }
    }

    public void print() {
        List<Integer> list = Arrays.asList(1, 2, 3, 4);
        list.forEach(new LambdaStudy004Inner().new lambda$1());
    }
}

具體引入 invokedynamic 實(shí)現(xiàn) Lambda 表達(dá)是的原因可以看 R 大的解釋, 傳送門: Java 8的Lambda表達(dá)式為什么要基于invokedynamic

最后編輯于
?著作權(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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