1. 程式人生 > >Java 8 習慣用語,第 8 部分 Java 知道您的型別

Java 8 習慣用語,第 8 部分 Java 知道您的型別

原文地址:https://www.ibm.com/developerworks/cn/java/j-java8idioms8/index.html

Java 8 習慣用語,第 8 部分

Java 知道您的型別

學習如何在 lambda 表示式中使用型別推斷,掌握改進引數命名的技巧

關於本系列

Java 8 是自 Java 語言誕生以來進行的一次最重大更新—包含了非常豐富的新功能,您可能想知道從何處開始著手瞭解它。在本系列中,作家兼教師 Venkat Subramaniam 提供了一種慣用的 Java 8 程式設計方法:這些簡短的探索將邀您重新思考您已習以為常的 Java 約定,同時逐步將新技術和語法整合到您的程式中。

Java™8 是第一個支援型別推斷的 Java 版本,而且它僅對 lambda 表示式支援此功能。在 lambda 表示式中使用型別推斷具有強大的作用,它將幫助您做好準備以應對未來的 Java 版本,在今後的版本中還會將型別推斷用於變數等更多可能。這裡的訣竅在於恰當地命名引數,相信 Java 編譯器會推斷出剩餘的資訊。

大多數時候,編譯器完全能夠推斷型別。在它無法推斷出來的時候,就會報錯。

瞭解 lambda 表示式中的型別推斷的工作原理,至少檢視一個無法推斷型別的示例。即使如此,也有解決辦法。

顯式型別和冗餘

假設您詢問某個人“您叫什麼名字?”,他會回答“我名叫約翰”。這種情況經常發生,但簡單地說“約翰”會更高效。您需要的只是一個名稱,所以該句子的剩餘部分都是多餘的。

不幸的是,我們總是在程式碼中做這類多餘的事情。Java 開發人員可以使用 forEach 迭代並輸出某個範圍內的每個值的雙倍值,如下所示:

IntStream.rangeClosed(1, 5) .forEach((int number) -> System.out.println(number * 2));

rangeClosed 方法生成一個從 1 到 5 的 int 值流。lambda 表示式的唯一職責就是接收一個名為 number 的 int 引數,使用PrintStream 的 println

 方法輸出該值的雙倍值。從語法上講,該 lambda 表示式沒有錯,但型別細節有些冗餘。

Java 8 中的型別推斷

當您從某個數字範圍中提取一個值時,編譯器知道該值的型別為 int。不需要在程式碼中顯式宣告該值,儘管這是目前為止的約定。

在 Java 8 中,我們可以丟棄 lambda 表示式中的型別,如下所示:

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

由於 Java 是靜態型別語言,它需要在編譯時知道所有物件和變數的型別。在 lambda 表示式的引數列表中省略型別並不會讓 Java 更接近動態型別語言。但是,新增適當的型別推斷功能會讓 Java 更接近其他靜態型別語言,比如 Scala 或 Haskell。

信任編譯器

如果您在 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