【譯】java8之lambda表達式

lambda表達式是java8中最重要的特性之一,它讓代碼變得簡潔并且允許你傳遞行為。曾幾何時,Java總是因為代碼冗長和缺少函數式編程的能力而飽受批評。隨著函數式編程變得越來越受歡迎,Java也被迫開始擁抱函數式編程。否則,Java會被大家逐步拋棄。

Java8是使得這個世界上最流行的編程語言采用函數式編程的一次大的跨越。一門編程語言要支持函數式編程,就必須把函數作為其一等公民。在Java8之前,只能通過匿名內部類來寫出函數式編程的代碼。而隨著lambda表達式的引入,函數變成了一等公民,并且可以像其他變量一樣傳遞。

lambda表達式允許開發者定義一個不局限于定界符的匿名函數,你可以像使用編程語言的其他程序結構一樣來使用它,比如變量申明。如果一門編程語言需要支持高階函數,lambda表達式就派上用場了。高階函數是指把函數作為參數或者返回結果是一個函數那些函數。

這個章節的代碼如下ch02 package.

隨著Java8中lambda表達式的引入,Java也支持高階函數。接下來讓我們來分析這個經典的lambda表達式示例--Java中Collections類的一個sort函數。sort函數有兩種調用方式,一種需要一個List作為參數,另一種需要一個List參數和一個Comparator。第二種sort函數是一個接收lambda表達式的高階函數的實例,如下:

List<String> names = Arrays.asList("shekhar", "rahul", "sameer");
Collections.sort(names, (first, second) -> first.length() - second.length());

上面的代碼是根據names的長度來進行排序,運行的結果如下:

[rahul, sameer, shekhar]

上面代碼片段中的(first,second) -> first.length() - second.length()表達式是一個Comparator<String>的lambda表達式。

  • (first,second)Comparatorcompare方法的參數。

  • first.length() - second.length()比較name字符串長度的函數體。

  • -> 是用來把參數從函數體中分離出來的操作符。

在我們深入研究Java8中的lambda表達式之前,我們先來追溯一下他們的歷史,了解它們為什么會存在。

lambda表達式的歷史

lambda表達式源自于λ演算.λ演算起源于用函數式來制定表達式計算概念的研究Alonzo Churchλ演算是圖靈完整的。圖靈完整意味著你可以用lambda表達式來表達任何數學算式。

λ演算后來成為了函數式編程語言強有力的理論基礎。諸如 Hashkell、Lisp等著名的函數式編程語言都是基于λ演算.高階函數的概念就來自于λ演算

λ演算中最主要的概念就是表達式,一個表達式可以用如下形式來表示:

<expression> := <variable> | <function>| <application>
  • variable -- 一個variable就是一個類似用x、y、z來代表1、2、n等數值或者lambda函數式的占位符。

  • function -- 它是一個匿名函數定義,需要一個變量,并且生成另一個lambda表達式。例如,λx.x*x是一個求平方的函數。

  • application -- 把一個函數當成一個參數的行為。假設你想求10的平方,那么用λ演算的方式的話你需要寫一個求平方的函數λx.x*x并把10應用到這個函數中去,這個函數程序就會返回(λx.x*x) 10 = 10*10 = 100。但是你不僅可以求10的平方,你可以把一個函數傳給另一個函數然后生成另一個函數。比如,(λx.x*x) (λz.z+10) 會生成這樣一個新的函數 λz.(z+10)*(z+10)。現在,你可以用這個函數來生成一個數加上10的平方。這就是一個高階函數的實例。

現在,你已經理解了λ演算和它對函數式編程語言的影響。下面我們繼續學習它們在java8中的實現。

在java8之前傳遞行為

Java8之前,傳遞行為的唯一方法就是通過匿名內部類。假設你在用戶完成注冊后,需要在另外一個線程中發送一封郵件。在Java8之前,可以通過如下方式:

sendEmail(new Runnable() {
            @Override
            public void run() {
                System.out.println("Sending email...");
            }
        });

sendEmail方法定義如下:

public static void sendEmail(Runnable runnable)

上面的代碼的問題不僅僅在于我們需要把行為封裝進去,比如run方法在一個對象里面;更糟糕的是,它容易混淆開發者真正的意圖,比如把行為傳遞給sendEmail函數。如果你用過一些類似Guava的庫,那么你就會切身感受到寫匿名內部類的痛苦。下面是一個簡單的例子,過濾所有標題中包含lambda字符串的task。

Iterable<Task> lambdaTasks = Iterables.filter(tasks, new Predicate<Task>() {
            @Override
            public boolean apply(Task task) {
                return input.getTitle().contains("lambda");
            }
});

使用Java8的Stream API,開發者不用太第三方庫就可以寫出上面的代碼,我們將在下一章chapter 3講述streams相關的知識。所以,繼續往下閱讀!

Java 8 Lambda表達式

在Java8中,我們可以用lambda表達式寫出如下代碼,這段代碼和上面提到的是同一個例子。

sendEmail(() -> System.out.println("Sending email..."));

上面的代碼非常簡潔,并且能夠清晰的傳遞編碼者的意圖。()用來表示無參函數,比如Runnable接口的中run方法不含任何參數,直接就可以用()來代替。->是將參數和函數體分開的lambda操作符,上例中,->后面是打印Sending email的相關代碼。

下面再次通過Collections.sort這個例子來了解帶參數的lambda表達式如何使用。要將names列表中的name按照字符串的長度排序,需要傳遞一個Comparator給sort函數。Comparator的定義如下

Comparator<String> comparator = (first, second) -> first.length() - second.length();

上面寫的lambda表達式相當于Comparator接口中的compare方法。compare方法的定義如下:

int compare(T o1, T o2);

T是傳遞給Comparator接口的參數類型,在本例中names列表是由String組成,所以T代表的是String

在lambda表達式中,我們不需要明確指出參數類型,javac編譯器會通過上下文自動推斷參數的類型信息。由于我們是在對一個由String類型組成的List進行排序并且compare方法僅僅用一個T類型,所以Java編譯器自動推斷出兩個參數都是String類型。根據上下文推斷類型的行為稱為類型推斷。Java8提升了Java中已經存在的類型推斷系統,使得對lambda表達式的支持變得更加強大。javac會尋找緊鄰lambda表達式的一些信息通過這些信息來推斷出參數的正確類型。

在大多數情況下,javac會根據上下文自動推斷類型。假設因為丟失了上下文信息或者上下文信息不完整而導致無法推斷出類型,代碼就不會編譯通過。例如,下面的代碼中我們將String類型從Comparator中移除,代碼就會編譯失敗。

Comparator comparator = (first, second) -> first.length() - second.length(); // compilation error - Cannot resolve method 'length()'

Lambda表達式在Java8中的運行機制

你可能已經發現lambda表達式的類型是一些類似上例中Comparator的接口。但并不是每個接口都可以使用lambda表達式,只有那些僅僅包含一個非實例化抽象方法的接口才能使用lambda表達式。這樣的接口被稱著函數式接口并且它們能夠被@FunctionalInterface注解注釋。Runnable接口就是函數式接口的一個例子。

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

@FunctionalInterface注解不是必須的,但是它能夠讓工具知道這一個接口是一個函數式接口并表現有意義的行為。例如,如果你試著這編譯一個用@FunctionalInterface注釋自己并且含有多個抽象方法的接口,編譯就會報出這樣一個錯Multiple non-overriding abstract methods found。同樣的,如果你給一個不含有任何方法的接口添加@FunctionalInterface注解,會得到如下錯誤信息,No target method found.

下面來回答一個你大腦里一個非常重大的疑問,Java8的lambda表達式是否只是一個匿名內部類的語法糖或者函數式接口是如何被轉換成字節碼的?

答案是NO,Java8不采用匿名內部類的原因主要有兩點:

  1. 性能影響: 如果lambda表達式是采用匿名內部類實現的,那么每一個lambda表達式都會在磁盤上生成一個class文件。當JVM啟動時,這些class文件會被加載進來,因為所有的class文件都需要在啟動時加載并且在使用前確認,從而會導致JVM的啟動變慢。

  2. 向后的擴展性: 如果Java8的設計者從一開始就采用匿名內部類的方式,那么這將限制lambda表達式未來的使發展范圍。

使用動態啟用

Java8的設計者決定采用在Java7中新增的動態啟用來延遲在運行時的加載策略。當javac編譯代碼時,它會捕獲代碼中的lambda表達式并且生成一個動態啟用的調用地址(稱為lambda工廠)。當動態啟用被調用時,就會向lambda表達式發生轉換的地方返回一個函數式接口的實例。比如,在Collections.sort這個例子中,它的字節碼如下:

public static void main(java.lang.String[]);
    Code:
       0: iconst_3
       1: anewarray     #2                  // class java/lang/String
       4: dup
       5: iconst_0
       6: ldc           #3                  // String shekhar
       8: aastore
       9: dup
      10: iconst_1
      11: ldc           #4                  // String rahul
      13: aastore
      14: dup
      15: iconst_2
      16: ldc           #5                  // String sameer
      18: aastore
      19: invokestatic  #6                  // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
      22: astore_1
      23: invokedynamic #7,  0              // InvokeDynamic #0:compare:()Ljava/util/Comparator;
      28: astore_2
      29: aload_1
      30: aload_2
      31: invokestatic  #8                  // Method java/util/Collections.sort:(Ljava/util/List;Ljava/util/Comparator;)V
      34: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
      37: aload_1
      38: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      41: return
}

上面代碼的關鍵部分位于第23行23: invokedynamic #7, 0 // InvokeDynamic #0:compare:()Ljava/util/Comparator;這里創建了一個動態啟用的調用。

接下來是將lambda表達式的內容轉換到一個將會通過動態啟用來調用的方法中。在這一步中,JVM實現者有自由選擇策略的權利。

這里我僅粗略的概括一下,具體的內部標準見這里 http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html.

匿名類 vs lambda表達式

下面我們對匿名類和lambda表達式做一個對比,以此來區分它們的不同。

  1. 在匿名類中,this 指代的是匿名類本身;而在lambda表達式中,this指代的是lambda表達式所在的這個類。

  2. You can shadow variables in the enclosing class inside the anonymous class, 而在lambda表達式中就會報編譯錯誤。(英文部分不會翻譯,希望大家一起探討下,謝謝)

  3. lambda表達式的類型是由上下文決定的,而匿名類中必須在創建實例的時候明確指定。

我需要自己去寫函數式接口嗎?

Java8默認帶有許多可以直接在代碼中使用的函數式接口。它們位于java.util.function包中,下面簡單介紹幾個:

java.util.function.Predicate<T>

此函數式接口是用來定義對一些條件的檢查,比如一個predicate。Predicate接口有一個叫test的方法,它需要一個T類型的值,返回值為布爾類型。例如,在一個names列表中找出所有以s開頭的name就可以像如下代碼這樣使用predicate。

Predicate<String> namesStartingWithS = name -> name.startsWith("s");

java.util.function.Consumer<T>

這個函數式接口用于表現那些不需要產生任何輸出的行為。Consumer接口中有一個叫做accept的方法,它需要一個T類型的參數并且沒有返回值。例如,用指定信息發送一封郵件:

Consumer<String> messageConsumer = message -> System.out.println(message);

java.util.function.Function<T,R>

這個函數式接口需要一個值并返回一個結果。例如,如果需要將所有names列表中的name轉換為大寫,可以像下面這樣寫一個Function:

Function<String, String> toUpperCase = name -> name.toUpperCase();

java.util.function.Supplier<T>

這個函數式接口不需要傳值,但是會返回一個值。它可以像下面這樣,用來生成唯一的標識符

Supplier<String> uuidGenerator= () -> UUID.randomUUID().toString();

在接下來的章節中,我們會學習更多的函數式接口。

Method references

有時候,你需要為一個特定方法創建lambda表達式,比如Function<String, Integer> strToLength = str -> str.length();,這個表達式僅僅在String對象上調用length()方法。可以這樣來簡化它,Function<String, Integer> strToLength = String::length;。僅調用一個方法的lambda表達式,可以用縮寫符號來表示。在String::length中,String是目標引用,::是定界符,length是目標引用要調用的方法。靜態方法和實例方法都可以使用方法引用。

Static method references

假設我們需要從一個數字列表中找出最大的一個數字,那我們可以像這樣寫一個方法引用Function<List<Integer>, Integer> maxFn = Collections::maxmax是一Collections里的一個靜態方法,它需要傳入一個List類型的參數。接下來你就可以這樣調用它,maxFn.apply(Arrays.asList(1, 10, 3, 5))。上面的lambda表達式等價于Function<List<Integer>, Integer> maxFn = (numbers) -> Collections.max(numbers);

Instance method references

在這樣的情況下,方法引用用于一個實例方法,比如String::toUpperCase是在一個String引用上調用 toUpperCase方法。還可以使用帶參數的方法引用,比如:BiFunction<String, String, String> concatFn = String::concatconcatFn可以這樣調用:concatFn.apply("shekhar", "gulati")String``concat方法在一個String對象上調用并且傳遞一個類似"shekhar".concat("gulati")的參數。

Exercise >> Lambdify me

下面通過一段代碼,來應用所學到的。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks);
        for (String title : titles) {
            System.out.println(title);
        }
    }

    public static List<String> taskTitles(List<Task> tasks) {
        List<String> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (task.getType() == TaskType.READING) {
                readingTitles.add(task.getTitle());
            }
        }
        return readingTitles;
    }

}

上面這段代碼首先通過工具方法getTasks取得所有的Task,這里我們不去關心getTasks方法的具體實現,getTasks能夠通過webservice或者數據庫或者內存獲取task。一旦得到了tasks,我們就過濾所有處于reading狀態的task,并且從task中提取他們的標題,最后返回所有處于reading狀態task的標題。

下面我們簡單的重構下--在一個list上使用foreach和方法引用。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks);
        titles.forEach(System.out::println);
    }

    public static List<String> taskTitles(List<Task> tasks) {
        List<String> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (task.getType() == TaskType.READING) {
                readingTitles.add(task.getTitle());
            }
        }
        return readingTitles;
    }

}

使用Predicate<T>來過濾tasks

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks, task -> task.getType() == TaskType.READING);
        titles.forEach(System.out::println);
    }

    public static List<String> taskTitles(List<Task> tasks, Predicate<Task> filterTasks) {
        List<String> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (filterTasks.test(task)) {
                readingTitles.add(task.getTitle());
            }
        }
        return readingTitles;
    }

}

使用Function<T,R>來將task中的title提取出來。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<String> titles = taskTitles(tasks, task -> task.getType() == TaskType.READING, task -> task.getTitle());
        titles.forEach(System.out::println);
    }

    public static <R> List<R> taskTitles(List<Task> tasks, Predicate<Task> filterTasks, Function<Task, R> extractor) {
        List<R> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (filterTasks.test(task)) {
                readingTitles.add(extractor.apply(task));
            }
        }
        return readingTitles;
    }
}

把方法引用當著提取器來使用。

public static void main(String[] args) {
    List<Task> tasks = getTasks();
    List<String> titles = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, Task::getTitle);
    titles.forEach(System.out::println);
    List<LocalDate> createdOnDates = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, Task::getCreatedOn);
    createdOnDates.forEach(System.out::println);
    List<Task> filteredTasks = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, Function.identity());
    filteredTasks.forEach(System.out::println);
}

我們也可以自己編寫函數式接口,這樣可以清晰的把開發者的意圖傳遞給讀者。我們可以寫一個繼承自Function接口的TaskExtractor接口。這個接口的輸入類型是固定的Task類型,輸出類型由實現的lambda表達式來決定。這樣開發者就只需要關注輸出結果的類型,因為輸入的類型永遠都是Task。

public class Exercise_Lambdas {

    public static void main(String[] args) {
        List<Task> tasks = getTasks();
        List<Task> filteredTasks = filterAndExtract(tasks, task -> task.getType() == TaskType.READING, TaskExtractor.identityOp());
        filteredTasks.forEach(System.out::println);
    }

    public static <R> List<R> filterAndExtract(List<Task> tasks, Predicate<Task> filterTasks, TaskExtractor<R> extractor) {
        List<R> readingTitles = new ArrayList<>();
        for (Task task : tasks) {
            if (filterTasks.test(task)) {
                readingTitles.add(extractor.apply(task));
            }
        }
        return readingTitles;
    }

}


interface TaskExtractor<R> extends Function<Task, R> {

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

推薦閱讀更多精彩內容