- 原文鏈接: Lambdas
- 原文作者: shekhargulati
- 譯者: leege100
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)
是Comparator
中compare
方法的參數。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不采用匿名內部類的原因主要有兩點:
性能影響: 如果lambda表達式是采用匿名內部類實現的,那么每一個lambda表達式都會在磁盤上生成一個class文件。當JVM啟動時,這些class文件會被加載進來,因為所有的class文件都需要在啟動時加載并且在使用前確認,從而會導致JVM的啟動變慢。
向后的擴展性: 如果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表達式做一個對比,以此來區分它們的不同。
在匿名類中,
this
指代的是匿名類本身;而在lambda表達式中,this
指代的是lambda表達式所在的這個類。You can shadow variables in the enclosing class inside the anonymous class, 而在lambda表達式中就會報編譯錯誤。(英文部分不會翻譯,希望大家一起探討下,謝謝)
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::max
。max
是一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::concat
。concatFn
可以這樣調用: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;
}
}