第三章:lambda表达式
本文是學習Java8,參考JAVA8 IN ACTION這本書,學習整理以及自己的總結,推薦這本書;
1:Lambda 表達式
前篇文章講到,使用匿名類來表示不同的行為并不令人滿意:代碼十分啰嗦,這會影響程序 員在實踐中使用行為參數化的積極性。在本章中,我們會教給你Java 8中解決這個問題的新工 具——Lambda表達式。它可以讓你很簡潔地表示一個行為或傳遞代碼。現在你可以把Lambda 表達式看作匿名功能,它基本上就是沒有聲明名稱的方法,但和匿名類一樣,它也可以作為參 數傳遞給一個方法。
本章的行文思想就是教你如何一步一步地寫出更簡潔、更靈活的代碼。
2:Lambda 語法
可以把Lambda表達式簡單的理解為可以可以傳遞匿名函數的一種形式:它沒有名稱,但是有參數列表,函數主體,返回類型,甚至還可以有一個可以拋出異常的函數列表.
匿名:我們說匿名,是因為它不像普通的方法那樣有一個明確的名稱:寫得少而想得多! 函數: 我們說它是函數,是因為Lambda函數不像方法那樣屬于某個特定的類。但和方法一樣,Lambda有參數列表、函數主體、返回類型,還可能有可以拋出的異常列表。 傳遞:Lambda表達式可以作為參數傳遞給方法或存儲在變量中. 簡潔:無需像匿名類那樣寫很多模版代碼. 在前面的講解中,我們寫過一個簡單的Lambda表達式
可以看到,Lambda表達式有三個部分:參數列表:這里它采用了 Comparator 中 compare 方法的參數,兩個 Apple 。
箭頭:把參數和函數主題分開。
Lambda主體: 比較兩個 Apple 的重量。表達式就是Lambda的返回值了。
為了進一步說明,下面給出了Java 8中五個有效的Lambda表達式的例子。
java語言設計者選擇這樣的語法,是因為C#和Scala等語言中的類似功能廣受歡迎.Lambda的基本語法就是:
(parameters) -> expression 復制代碼或(請注意語句的花括號)
(parameters) -> { statements; } 復制代碼3:函數式接口
一言以蔽之,函數式接口就是只定義一個抽象方法的接口。
如我們前面創建的:
public interface Predicate<T>{boolean test (T t);} 復制代碼3.1:函數描述符
函數式接口的抽象方法的簽名基本上就是Lambda表達式的簽名,我們將這種抽象方法叫做函數描述符.
這很好理解:因為函數式接口只有一個抽象方法,因此我們只需要知道參數列表,和返回值就可以描述這個函數.
例如, Runnable 接口可以看作一個什么也不接受什么也不返回( void )的函數的 簽名,因為它只有一個叫作 run 的抽象方法,這個方法什么也不接受,什么也不返回( void )。
3.2:使用函數式接口
函數式接口定義且只定義了一個抽象方法.函數式接口很有用,因為抽象方法的簽名可以描述為Lambda表達式的簽名
函數式接口的抽象方法的簽名稱為函數描述符.
Java API中已經有了幾個函數式接口
Java 8的庫設計師幫你在 java.util.function 包中引入了幾個新的函數式接口。我們接下 來會介紹 Predicate 、 Consumer 和 Function ,更完整的可以查看API.
為了總結關于函數式接口和Lambda的討論,表3-3總結了一些使用案例、Lambda的例子,以 及可以使用的函數式接口。
4:類型檢查
Lambda表達式的類型是從Lambda的上下文中推斷出來的,上下文中Lambda表達式需要的類型稱為目標類型.
舉個上節中的例子:
5:類型推斷
java編譯器可以從上下文中推斷出用什么函數式接口來配合Lambda表達式,這意味著他可以推斷出適合Lambda表達式的簽名,因為函數描述符也可以從目標類型中得到. 這樣做的好處在于,編譯器可以了解Lambda表達式的參數類型
Lambda表達式有多個參數,代碼可讀性的好處就更為明顯。例如,你可以這樣來創建一個 Comparator 對象: 請注意,有時候顯式寫出類型更易讀,有時候去掉它們更易讀。沒有什么法則說哪種更好; 對于如何讓代碼更易讀,程序員必須做出自己的選擇6:局部變量
Lambda表達式也允許使用自有變量(不是參數,是在外層作用域定義的變量),就想匿名類一樣,他們被稱為捕獲Lambda
int portNumber = 1337; Runnable r = () -> System.out.println(portNumber); 復制代碼需要注意的是:盡管Lambda可以沒有限制地捕獲(也就是在其主體中引用)實例變量和靜態變量。但是局部變量必須顯示聲明為final,或者事實上就是final. 換句話說:Lambda表達式只能捕獲指派給他們的局部變量一次((注:捕獲實例變量可以被看作捕獲最終局部變量 this 。)), 例如,下面的代碼無法編譯,因為 portNumber 變量被賦值兩次:
6.1:對局部變量的限制
你可能會問自己,為什么局部變量有這些限制?????
第一,實例變量和局部變量背后的實現有一個關鍵的不同,實例變量存儲在堆中,局部變量存儲在棧中, 如果Lambda可以直接訪問局 部變量,而且Lambda是在一個線程中使用的,則使用Lambda的線程,可能會在分配該變量的線 程將這個變量收回之后,去訪問該變量。因此,Java在訪問自由局部變量時,實際上是在訪問它 的副本,而不是訪問原始變量。如果局部變量僅僅賦值一次那就沒有什么區別了——因此就有了 這個限制。
第二,這一限制不鼓勵你使用改變外部變量的典型命令式編程模式(我們會在以后的各章中 解釋,這種模式會阻礙很容易做到的并行處理)
閉包: 你可能已經聽說過閉包這個詞,你可能會想Lambda是否滿足閉包的定義.科學的講,閉包就是一個函數的實例.且它可以無限制的訪問那個函數的非本地變量. 例如,閉包可以作為參數傳遞給另一個函數。它也可以訪 問和修改其作用域之外的變量。現在,Java 8的Lambda和匿名類可以做類似于閉包的事情:它們可以作為參數傳遞給方法,并且可以訪問其作用域之外的變量. 但是有一個限制:就是它們不能修改定義Lambda的方法的局部變量的內容.這些變量必須是隱式最終的.可以認為Lambda是對值封閉,而不是對變量封閉.
如前所述: 這種限制的原因在于局部變量保存在棧上,并且隱式表示它們僅限于其所在線程.如果允許捕獲局部變量,就會引發造成線程不安全的新得可能性,而這時我們不想看到的 實例變量是可以的,因為它們保存在堆中,而堆是在線程中共享的.
7:方法引用
方法引用可以被看作是調用Lambda表達式的一種快捷方法.
它的基本思想是:
如果一個Lambda表達式代表是 直接調用這個方法,那最好還是用名稱來調用它. 事實上,方法引用就是讓你根據已有的方法來創建Lambda表達式.
它是如何工作的呢?
當你需要使用方法引用時,目標引用放在分隔符 :: 前,方法名放在后面.
例如, Apple::getWeight 就是引用了 Apple 類中定義的方法 getWeight 。請記住,不需要括號,因為 你沒有實際調用這個方法。方法引用就是Lambda表達式 (Apple a) -> a.getWeight() 的快捷 寫法。
/*** 方法引用*/@Testpublic void test3(){List<Apple> inventory1 = initInventory();inventory1.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight()));System.out.println(inventory1);List<Apple> inventory = initInventory();inventory.sort(Comparator.comparing(Apple::getWeight));System.out.println(inventory);} 復制代碼8:Lambda 和方法引用實戰
我們想要實現的最終解決方案是這樣的:
inventory.sort(comparing(Apple::getWeight)); 復制代碼第 1 步:傳遞代碼:
Java 8的API已經為你提供了一個 List 可用的 sort 方法,你不用自己去實現它。 那么最困難的部分已經搞定了!但是,如何把排序策略傳遞給 sort 方法呢?你看, sort 方法的 簽名是這樣的:
void sort(Comparator<? super E> c) 復制代碼它需要一個Comparator對象來比較兩個Apple,這就是zaijava中傳遞策略的方式:他們必須包裹在一個對象里, 我們說 sort 的行為被參數化了:傳遞給它的排序策略不同,其行為也會不同.
你的第一個解決方案看上去是這樣的:
public class AppleComparator implements Comparator<Apple> {public int compare(Apple a1, Apple a2){return a1.getWeight().compareTo(a2.getWeight());} } 復制代碼 inventory.sort(new AppleComparator()) 復制代碼第 2 步:使用匿名類
你可以使用匿名類來改進解決方案,而不是實現一個 Comparator 卻只實 例化一次:
inventory.sort(new Comparator<Apple>() {public int compare(Apple a1, Apple a2){return a1.getWeight().compareTo(a2.getWeight());}}); 復制代碼第 3 步:使用 Lambda 表達式
但你的解決方案仍然挺啰嗦的。Java 8引入了Lambda表達式,它提供了一種輕量級語法來實 現相同的目標:傳遞代碼
inventory.sort((Apple a1, Apple a2)-> a1.getWeight().compareTo(a2.getWeight())); 復制代碼我們前面解釋過了,Java編譯器可以根據Lambda出現的上下文來推斷Lambda表達式參數的 類型。那么你的解決方案就可以重寫成這樣:
inventory.sort((a1, a2) -> a1.getWeight().compareTo(a2.getWeight())); 復制代碼你的代碼還能變得更易讀一點嗎???
Comparator 具有一個叫做Comparing的靜態輔助方法,方法描述如下:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(Function<? super T, ? extends U> keyExtractor){Objects.requireNonNull(keyExtractor);return (Comparator<T> & Serializable)(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));} 復制代碼可以看到,它可以接受一個Function來提取一個鍵值,并生成一個Compartor對象.
它可以像下面這樣用(注意你現在傳遞的Lambda只有一 個參數:Lambda說明了如何從蘋果中提取需要比較的鍵值):
Comparator<Apple> c = Comparator.comparing((Apple a) -> a.getWeight()); 復制代碼因此,現在你可以該代碼改為這樣:
import static java.util.Comparator.comparing;inventory.sort(comparing((a) -> a.getWeight())); 復制代碼第 4 步:使用方法引用
方法引用就是替代那些轉發參數的Lambda表達式的語法糖. 你可以使用方法引用讓你的代碼更簡潔,假設你靜態導入了 java.util.Comparator.comparing. 那么你現在的代碼可以這樣:
inventory.sort(comparing(Apple::getWeight)); 恭喜你,這就是你的最終解決方案!這比Java 8之前的代碼好在哪兒呢?它比較短;它的意 思也很明顯,并且代碼讀起來和問題描述差不多:“對庫存進行排序,比較蘋果的重量。”
9:小結
-
Lambda 表達式可以理解為一種匿名函數:它沒有名稱,但有參數列表、函數主體、返回 類型,可能還有一個可以拋出的異常的列表。
-
Lambda 表達式讓你可以簡潔地傳遞代碼
-
函數式接口就是僅僅聲明了一個抽象方法的接口
-
只有在接受函數式接口的地方才可以使用 Lambda 表達式
-
Lambda 表達式允許你直接內聯,為函數式接口的抽象方法提供實現,并且將整個表達式 作為函數式接口的一個實例
-
Java 8 自帶一些常用的函數式接口,放在 java.util.function 包里,包括 Predicate 、 Function<T,R> 、 Supplier 、 Consumer 和 BinaryOperator
-
為了避免裝箱操作,對 Predicate 和 Function<T, R> 等通用函數式接口的原始類型 特化: IntPredicate 、 IntToLongFunction 等。
-
環繞執行模式(即在方法所必需的代碼中間,你需要執行點兒什么操作,比如資源分配 和清理)可以配合 Lambda 提高靈活性和可重用性
-
Comparator 、 Predicate 和 Function 等函數式接口都有幾個可以用來結合 Lambda 表達 式的默認方法。
轉載于:https://juejin.im/post/5b2f1c8551882574be491637
總結
以上是生活随笔為你收集整理的第三章:lambda表达式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 最细的实现剖析:jQuery 2.0.3
- 下一篇: HBuilder 无法检测到XCode上