Java™8 是第一個支持類型推斷的 Java 版本,並且它僅對 lambda 表達式支持此功能。在 lambda 表達式中使用類型推斷具備強大的做用,它將幫助您作好準備以應對將來的 Java 版本,在從此的版本中還會將類型推斷用於變量等更多可能。這裏的訣竅在於恰當地命名參數,相信 Java 編譯器會推斷出剩餘的信息。html
大多數時候,編譯器徹底可以推斷類型。在它沒法推斷出來的時候,就會報錯。java
瞭解 lambda 表達式中的類型推斷的工做原理,至少查看一個沒法推斷類型的示例。即便如此,也有解決辦法。ide
假設您詢問某我的「您叫什麼名字?」,他會回答「我名叫約翰」。這種狀況常常發生,但簡單地說「約翰」會更高效。您須要的只是一個名稱,因此該句子的剩餘部分都是多餘的。函數
不幸的是,咱們老是在代碼中作這類多餘的事情。Java 開發人員可使用 forEach
迭代並輸出某個範圍內的每一個值的雙倍值,以下所示:測試
IntStream.rangeClosed(1, 5) .forEach((int number) -> System.out.println(number * 2));
rangeClosed
方法生成一個從 1 到 5 的 int
值流。lambda 表達式的惟一職責就是接收一個名爲 number
的 int
參數,使用PrintStream
的 println
方法輸出該值的雙倍值。從語法上講,該 lambda 表達式沒有錯,但類型細節有些冗餘。code
當您從某個數字範圍中提取一個值時,編譯器知道該值的類型爲 int
。不須要在代碼中顯式聲明該值,儘管這是目前爲止的約定。htm
在 Java 8 中,咱們能夠丟棄 lambda 表達式中的類型,以下所示:對象
IntStream.rangeClosed(1, 5) .forEach((number) -> System.out.println(number * 2));
因爲 Java 是靜態類型語言,它須要在編譯時知道全部對象和變量的類型。在 lambda 表達式的參數列表中省略類型並不會讓 Java 更接近動態類型語言。可是,添加適當的類型推斷功能會讓 Java 更接近其餘靜態類型語言,好比 Scala 或 Haskell。blog
若是您在 lambda 表達式的一個參數中省略類型,Java 須要經過上下文細節來推斷該類型。接口
返回到上一個示例,當咱們在 IntStream
上調用 forEach
時,編譯器會查找該方法來肯定它採用的參數。IntStream
的 forEach
方法指望使用函數接口 IntConsumer
,該接口的抽象方法 accept
採用了一個 int
類型的參數並返回 void
。
若是在參數列表中指定了該類型,編譯器將會確認該類型符合預期。
若是省略該類型,編譯器會推斷出預期的類型 —在本例中爲 int
。
不管是您提供類型仍是編譯器推斷出該類型,Java 都會在編譯時知道 lambda 表達式參數的類型。要測試這種狀況,能夠在 lambda 表達式中引入一個錯誤,同時省略參數的類型:
IntStream.rangeClosed(1, 5) .forEach((number) -> System.out.println(number.length() * 2));
編譯此代碼時,Java 編譯器會返回如下錯誤:
Sample.java:7: error: int cannot be dereferenced .forEach((number) -> System.out.println(number.length() * 2)); ^1 error
編譯器知道名爲 number
的參數的類型。它報錯是由於它沒法使用點運算符解除對某個 int
類型的變量的引用。能夠對對象執行此操做,但不能對 int
變量這麼作。
在 lambda 表達式中省略類型有兩個主要好處:
(number)
比 (int number)
簡單得多。此外,通常來說,若是咱們僅有一個參數,省略類型意味着也能夠省略 ()
,以下所示:
IntStream.rangeClosed(1, 5) .forEach(number -> System.out.println(number * 2));
請注意,您將須要爲採用多個參數的 lambda 表達式添加括號。
lambda 表達式中的類型推斷違背了 Java 中的常規作法,在常規作法中,會指定每一個變量和參數的類型。儘管一些開發人員辯稱 Java 指定類型的約定讓代碼變得更可讀、更容易理解,但我認爲這種偏好反映出一種習慣而不是必要性。
以一個包含一系列轉換的函數管道爲例:
List<String> result = cars.stream() .map((Car c) -> c.getRegistration()) .map((String s) -> DMVRecords.getOwner(s)) .map((Person o) -> o.getName()) .map((String s) -> s.toUpperCase()) .collect(toList());
在這裏,咱們首先提供了一組 Car
實例和相關的註冊信息。咱們獲取每輛車的車主和車主姓名,並將該姓名轉換爲大寫。最後,將結果放入一個列表中。
這段代碼中的每一個 lambda 表達式都爲其參數指定了一個類型,但咱們爲參數使用了單字母變量名。這在 Java 中很常見。但這種作法不合適,由於它丟棄了特定於域的上下文。
咱們能夠作得比這更好。讓咱們看看使用更強大的參數名重寫代碼後發生的狀況:
List<String> result = cars.stream() .map((Car car) -> car.getRegistration()) .map((String registration) -> DMVRecords.getOwner(registration)) .map((Person owner) -> owner.getName()) .map((String name) -> name.toUpperCase()) .collect(toList());
這些參數名包含了特定於域的信息。咱們沒有使用 s
來表示 String
,而是指定了特定於域的細節,好比 registration
和name
。相似地,咱們沒有使用 p
或 o
,而是使用 owner
代表 Person
不僅是一我的,仍是這輛車的車主。
這個示例中的每一個 lambda 表達式都比它所取代的表達式更好。在讀取 lambda 表達式(例如 (Person owner) -> owner.getName()
)時,咱們知道咱們得到了車主的姓名,而不僅是隨便某我的的姓名。
Scala 和 TypeScript 等一些語言更加劇視參數名而不是類型。在 Scala 中,咱們在定義類型以前定義參數,例如經過編寫:
def getOwner(registration: String)
而不是:
def getOwner(String registration)
類型和參數名都頗有用,但在 Scala 中,參數名更重要一些。咱們用 Java 編寫 lambda 表達式時,也能夠考慮這一想法。請注意咱們在 Java 中的車輛註冊示例中丟棄類型細節和括號時發生的狀況:
List<String> result = cars.stream() .map(car -> car.getRegistration()) .map(registration -> DMVRecords.getOwner(registration)) .map(owner -> owner.getName()) .map(name -> name.toUpperCase()) .collect(toList());
由於咱們添加了描述性的參數名,因此咱們沒有丟失太多上下文,並且顯式類型(如今是冗餘內容)已悄然消失。結果是咱們得到了更乾淨、更樸實的代碼。
儘管使用類型推斷能夠提升效率和可讀性,但這種技術並不適用於全部場合。在某些狀況下,徹底沒法使用類型推斷。幸運的是,您能夠依靠 Java 編譯器來獲知什麼時候出現這種狀況。
咱們首先看一個測試編譯器並得到成功的示例,而後看一個測試失敗的示例。最重要的是,在兩種狀況下,都可以相信編譯器會定期望方式工做。
在咱們的第一個示例中,假設咱們想建立一個 Comparator
來比較 Car
實例。咱們首先須要一個 Car
類:
class Car { public String getRegistration() { return null; }}
接下來,咱們將建立一個 Comparator
,以便基於 Car
實例的註冊信息對它們進行比較:
public static Comparator<Car> createComparator() { return comparing((Car car) -> car.getRegistration());}
用做 comparing
方法的參數的 lambda 表達式在其參數列表中包含了類型信息。咱們知道 Java 編譯器很是擅長類型推斷,那麼讓咱們看看在省略參數類型的狀況下會發生什麼,以下所示:
public static Comparator<Car> createComparator() { return comparing(car -> car.getRegistration());}
comparing
方法採用了 1 個參數。它指望使用 Function<? super T, ? extends U>
並返回 Comparator<T>
。由於 comparing
是 Comparator<T>
上的一個靜態方法,因此編譯器目前沒有關於 T
或 U
多是什麼的線索。
爲了解決此問題,編譯器稍微擴展了推斷範圍,將範圍擴大到傳遞給 comparing
方法的參數以外。它觀察咱們是如何處理調用comparing
的結果的。根據此信息,編譯器肯定咱們僅返回該結果。接下來,它看到由 comparing
返回的 Comparator<T>
又做爲 Comparator<Car>
由 createComparator
返回 。
注意了!編譯器如今已明白咱們的意圖:它推斷應該將 T
綁定到 Car
。根據此信息,它知道 lambda 表達式中的 car
參數的類型應該爲 Car
。
在這個例子中,編譯器必須執行一些額外的工做來推斷類型,但它成功了。接下來,讓咱們看看在提升挑戰難度,讓編譯器達到其能力極限時,會發生什麼。
首先,咱們在前一個 comparing
調用後面添加了一個新調用。在本例中,咱們還爲 lambda 表達式的參數從新引入顯式類型:
public static Comparator<Car> createComparator() { return comparing((Car car) -> car.getRegistration()).reversed();}
藉助顯式類型,此代碼沒有編譯問題,但如今讓咱們丟棄類型信息,看看會發生什麼:
public static Comparator<Car> createComparator() { return comparing(car -> car.getRegistration()).reversed();}
如您下面所見,進展並不順利。Java 編譯器拋出了錯誤:
Sample.java:21: error: cannot find symbol return comparing(car -> car.getRegistration()).reversed(); ^ symbol: method getRegistration() location: variable car of type ObjectSample.java:21: error: incompatible types: Comparator<Object> cannot be converted to Comparator<Car> return comparing(car -> car.getRegistration()).reversed(); ^2 errors
像上一個場景同樣,在包含 .reversed()
以前,編譯器會詢問咱們將如何處理調用 comparing(car -> car.getRegistration())
的結果。在上一個示例中,咱們以 Comparable<Car>
形式返回結果,因此編譯器能推斷出 T
的類型爲 Car
。
但在修改事後的版本中,咱們將傳遞 comparable
的結果做爲調用 reversed()
的目標。comparable
返回Comparable<T>
,reversed()
沒有展現任何有關 T
的可能含義的額外信息。根據此信息,編譯器推斷 T
的類型確定是 Object
。遺憾的是,此信息對於該代碼而言並不足夠,由於 Object
缺乏咱們在 lambda 表達式中調用的 getRegistration()
方法。
類型推斷在這一刻失敗了。在這種狀況下,編譯器實際上須要一些信息。類型推斷會分析參數、返回元素或賦值元素來肯定類型,但在上下文提供的細節不足時,編譯器就會達到其能力極限。
在咱們放棄這種特殊狀況以前,讓咱們嘗試另外一種方法:不使用 lambda 表達式,而是嘗試使用方法引用:
public static Comparator<Car> createComparator() { return comparing(Car::getRegistration).reversed();}
編譯器對此解決方案很是滿意。它在方法引用中使用 Car::
來推斷類型。
Java 8 爲 lambda 表達式的參數引入了有限的類型推斷能力,在將來的 Java 版本中,會將類型推斷擴展到局部變量。如今應該學會省略類型細節並信任編譯器,這有助於您輕鬆步入將來的 Java 環境。
依靠類型推斷和適當命名的參數,編寫簡明、更富於表達且更少雜質的代碼。只要您相信編譯器能自行推斷出類型,就可使用類型推斷。僅在您肯定編譯器確實須要您的幫助的狀況下提供類型細節。
原做者:Venkat Subramaniam
原文連接: Java 8 習慣用語
原出處: IBM Developer