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

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

近些日子一直在使用和研究 golang,很長時間沒有關心 java 相關的知識,前些天看到 java9 已經正式發布,意識到自己的 java 知識已經落后很多,心里莫名焦慮,決定將拉下的知識補上。

Lambda 表達式的淵源

Java8 作為近年來最重要的更新之一,為開發者帶來了很多新特性,可能在很多其他語言中早已實現,但來的晚總比不來好。Lambda 表達式就是 Java8 帶來的最重要的特性之一。

Lambda 表達式為 Java8 帶來了部分函數式編程的支持。Lambda 表達式雖然不完全等同于閉包,但也基本實現了閉包的功能。和其他一些函數式語言不一樣的是,Java 中的 Lambda 表達式也是對象,必須依附于一類特別的對象類型,函數式接口。

為什么需要 Lambda 表達式

內循環 VS. 外循環

先看一個非常簡單的例子, 打印 list 內所有元素:

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

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

作為一個 Java 開發者,你這一生可能已經寫過無數次類似代碼。看上去好像挺好的,沒有什么需要改進的,我們顯式的在外部迭代遍歷 list 內元素,并挨個處理其中元素。那為什么提倡內部迭代呢,因為內部迭代有助于 JIT 的優化,JIT 可以將處理元素的過程并行化。

在 Java8 之前,需要借助 Guava 或其他第三方庫來實現內部迭代,而在 Java8 中, 我們可以用以下代碼實現:

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

以上代碼還是稍顯繁瑣,需要創建一個匿名類,使用 lambda 表達式后,可以大大簡化代碼

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

Java 8 中 還引入了雙冒號運算符,用于類方法引用,以上方法可以進一步簡化為

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

內循環描述你要干什么,更符合自然語言描述的邏輯

passing behavior,not only value

通過 lambda 表達式,我們可以在傳參時,不僅可以將值傳入,還可將相關行為也傳入,這樣可以實現更加抽象和通用,更易復用的 API。看一下代碼例子,需要實現一個求 list 內所有元素和的方法,嗯,看上去很簡單。

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

這個時候,又有需求實現一個 list 內所有偶數和的方法,簡單,代碼復制一遍,稍作修改。

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

也沒發多少功夫,還需要改進么,這個時候又需要所有奇數和呢,不同的需求過來,你需要一遍又一遍的復制代碼。有沒有更加優雅的解決方法呢?我們又想起了我們的 lambda 表達式,java 8 引入了一個新的函數接口 Predicate<T>, 使用它來定義 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;
}

這樣以上兩個方法都可以通過這個方法實現,并且可以非常容易的擴展,當你需要用其他條件實現元素篩選求和時,只需要實現篩選條件的 lambda 表達式,如下

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

有同學會說,以前不用 lambda 表達式我們用接口也能實現。沒錯,用接口 + 匿名類也能實現類似效果,但 lambda 表達式更加直觀,代碼簡捷,可讀性也強,開發者也更有動力使用類似代碼。

利于寫出優雅可讀性更高的代碼

先看一段代碼:

        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;
                }
            }
        }

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

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

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

Lambda 表達式語法

Lambda 表達式的語法定義在 Java 8 規范 15.27 中,并給出了一些例子

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

總結一下:

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

Functional Interface (函數接口)

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

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

也就是說,現在啟動一個線程時,可以采用新的 Lambda 表達式

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

之前已經存在的接口還有

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

Java 8 中還新加了一些函數接口

java.util.function.Consumer<T>  // 消費一個元素,無返回
java.util.function.Supplier<T>  // 每次返回一個 T 類型的對象
java.util.function.Predicate<T> // 輸入一個元素,返回 boolean 值,常用于 filter
java.util.function.Function<T,R> // 輸入一個 T 類型元素,返回一個 R 類型對象

Lambda 表達式與匿名類

看上面的內容,一定會有人認為這些功能我使用匿名類也可以實現,那 Lambda 表達式和匿名類有什么區別呢。最明顯的區別就是 this 指針,this 指針在匿名類中代表是匿名類,而在 Lambda 表達式中為包含 Lambda 表達式的類。同時,匿名類可以實現多個方法,而 Lambda 表達式只能有一個方法。
直觀上,很多人會覺得 Lambda 表達式可能只是一個語法糖,最終轉換為一個匿名類。事實上,考慮到實現效率問題,和向前兼容問題,Java 8 并沒有采用匿名類語法糖,也沒有和其他語言一樣,采用專門的函數處理類型來實現 lambda 表達式。

lambda 實現

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

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

javap -p 結果

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

很明顯,lambda 表達式編譯后,會生成類的一個私有靜態方法,然而,事情并沒有那么簡單,雖然生成了一個靜態方法,lambda 表達式本身又由什么表示呢,java 中沒有函數指針,總要有一個類作為載體調用該靜態方法。

javap -p -v 查看字節碼

...

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 方法調用采用 invokestatic 指令不一樣,lambda 表達式的調用采用了 java 7 新引入的 invokedynamic 指令,該指令是為了加強 java 的動態語言特性引入,當 invokedynamic 指令被調用時,會調用 metafactory 函數動態生成一個實現了函數接口的對象,該對象實現的方法實際調用了之前生成的 static 方法,這個對象才是 lambda 表達式的實際翻譯后的表示,翻譯代碼如下

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 實現 Lambda 表達是的原因可以看 R 大的解釋, 傳送門: Java 8的Lambda表達式為什么要基于invokedynamic

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,362評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,577評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,486評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,852評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,600評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,944評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,944評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,108評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,652評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,385評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,616評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,111評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,798評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,205評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,537評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,334評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,570評論 2 379

推薦閱讀更多精彩內容