Java8 新語法習慣 (類型推斷)

學習如何在 lambda 表達式中使用類型推斷,掌握改進參數命名的技巧。java

概覽

Java8 是一個支持類型推斷的 Java 版本,並且它僅對 lambda 表達式支持此功能。在 lambda 表達式中使用類型推斷具備強大的做用,它將幫助您作好準備來應對將來的 Java 版本,在之後的版本中還會將類型推斷用於變量等更多可能。這裏的訣竅在於恰當地命名參數,相信 Java 編譯器會推斷出剩餘的信息。大多數時候編譯器可以徹底推斷出類型。在沒法推斷出來的時候,就會報錯。瞭解 lambda 表達式的推斷工做原理,至少要查看一個沒法推斷類型的示例。bash

顯示類型和冗餘

假設咱們詢問一我的叫什麼名字,它會回答:「我叫xxx」。這樣的例子在生活中常常會發生,可是簡單地說 「xxx」 會更高效。您須要的只是一個名稱,因此該句子的剩餘部分都是多餘的。函數

咱們在代碼中也常常會遇到這類多餘的事情。Java 開發人員可使用 forEach 迭代輸出某個範圍內的每一個值的雙倍。看下面的例子:學習

public static void main(String[] args) {
		// TODO Auto-generated method stub
		IntStream.rangeClosed(1, 5)
		  .forEach((int number) -> System.out.println(number * 2));
	}
複製代碼

測試結果:測試

2
4
6
8
10

複製代碼

rangeClosed 方法生成一個從 1 到 5 的 int 值流。lambda 表達式的惟一職責就是接收一個名爲 number 的 int 參數,使用 PrintStream 的 println 方法輸出該值的雙倍值。從語法上講,該 lambda 表達式沒有錯,但類型細節有些冗餘。spa

java8 中的類型推斷

當您在某個數字範圍內提取一個值時,編譯器知道該值的類型爲 int。不須要在代碼中顯示的聲明。code

在 Java8 中咱們能夠丟棄 lambda 表達式中的類型:cdn

IntStream.rangeClosed(1, 5)
  .forEach((number) -> System.out.println(number * 2));
複製代碼

因爲 Java 是靜態類型語言,它須要在編譯時知道全部的對象和變量的類型。在 lambda 表達式的參數列表中省略類型並不會讓 Java 更接近動態類型語言。可是,添加適當的類型推斷功能會讓 Java 更接近其餘靜態類型語言,好比 Haskel 等。對象

信任編譯器

若是您在 lambda 表達式的一個參數中省略類型,Java 須要經過上下文細節來推斷該類型。返回上一個示例,當咱們在 IntStream 上調用 forEach 時,編譯器會查找該方法來肯定它採用的參數。IntStream 的 forEach 方法指望使用函數接口 IntConsumer,該接口的抽象方法 accept 採用了一個 int 類型的參數並返回 void。blog

若是在參數列表中指定了該類型,編譯器將會確認該類型符合預期。若是省略了該類型,編譯器會推斷出預期的類型。

不管您是提供類型仍是編譯器推斷出該類型,Java 都會在編譯時知道 lambda 表達式參數的類型。要測試這種狀況,能夠在 lambda 表達式中引入一個錯誤,同時省略參數的類型:

編譯器直接就會報錯。編譯器知道名爲 number 的參數的類型。它報錯是由於它沒法使用點運算符解除對某個 int 類型的變量的引用。不能對 int 變量執行這個操做。

類型推斷的好處

在 lambda 表達式中省略類型有兩個主要好處:

  • 鍵入的內容更少。無需輸入類型信息,由於編譯器本身能輕鬆肯定該類型。
  • 代碼雜質更少,更簡單。

此外,通常來說,若是咱們僅有一個參數,省略類型意味着也能夠省略 (),以下所示:

IntStream.rangeClosed(1, 5)
  .forEach(number -> System.out.println(number * 2));
複製代碼

請注意,您將須要爲採用多個參數的 lambda 表達式添加括號。

類型推斷和可讀性

lambda 表達式中的類型推斷違背了 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());
複製代碼

這段代碼中的每一個 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 不僅是一我的,仍是這輛車的車主。

命名參數

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 調用後面添加了一個新調用:

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 Object Sample.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 博士

Venkat Subramaniam 博士站點:http://agiledeveloper.com/

知識改變命運,努力改變生活

相關文章
相關標籤/搜索