【譯】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;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章