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
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 |