Java8 新特性(一) - Lambda

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

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

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

也就是說,現在啟動一個線程時,可以採用新的 Lambda 運算式

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

之前已經存在的介面還有

java.lang.Comparablejava.util.concurrent.Callable

Java 8 中還新加了一些函數介面

java.util.function.Consumer<T>  // 消費一個元素,無返回java.util.function.Supplier<T>  // 每次返回一個 T 類型的對象java.util.function.Predicate<T> // 輸入一個元素,返回 boolean 值,常用於 filterjava.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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.