【譯】java8之lambda表示式

weixin_33816300發表於2016-01-20

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

相關文章