Java 8 習慣用語(8):Java 知道您的類型

Java 8 習慣用語(8):Java 知道您的類型

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 表達式的惟一職責就是接收一個名爲 numberint 參數,使用PrintStreamprintln 方法輸出該值的雙倍值。從語法上講,該 lambda 表達式沒有錯,但類型細節有些冗餘。code

Java 8 中的類型推斷

當您從某個數字範圍中提取一個值時,編譯器知道該值的類型爲 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 時,編譯器會查找該方法來肯定它採用的參數。IntStreamforEach 方法指望使用函數接口 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,而是指定了特定於域的細節,好比 registrationname。相似地,咱們沒有使用 po,而是使用 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>。由於 comparingComparator<T> 上的一個靜態方法,因此編譯器目前沒有關於 TU 多是什麼的線索。

爲了解決此問題,編譯器稍微擴展了推斷範圍,將範圍擴大到傳遞給 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

e3f6982e97de58e289858a0ec142affe.jpeg

相關文章
相關標籤/搜索