天天看點

Java 10 局部變量類型推斷是什麼?

Java 10 局部變量類型推斷是什麼?

#掃描上方二維碼進入報名#

協作翻譯

原文:Java 10 Local Variable Type Inference

連結:https://developer.oracle.com/java/jdk-10-local-variable-type-inference

譯者:邊城, dreamanzhao, 無若, imqipan

Java 10 引進一種新的閃閃發光的特性叫做局部變量類型推斷。聽起來很高大上吧?它是什麼呢? 下面的兩個情景是我們作為 Java 開發者認為 Java 比較難使用的地方。

上下文:陳詞濫調和代碼可讀性

也許日複一日,你希望不再需要重複做一些事情。例如在下面的代碼(使用 Java 9 的集合工廠),左邊的類型也許會感覺到備援和平淡。

Java 10 局部變量類型推斷是什麼?

這是一個非常簡單的例子,不過它也印證了傳統的 Java 哲學:你需要為所有包含的簡單表達式定義靜态類型。再讓我們來看看有一些複雜的例子。舉例來說,下面的代碼建立了一個從字元串到詞的柱狀圖。它使用 groupingBy 收集器将流聚合進 Map 。groupingBy 收集器還可以以一個分類函數為第一個參數建立映射的鍵和第二個收集器的 (counting()) 鍵計算關聯的數量。下面就是例子:

Java 10 局部變量類型推斷是什麼?

複雜表達式提取到一個變量或方法來提升代碼的可讀性和重用性,這是非常有意義的。在這裡例子中,建立柱狀圖的邏輯使用了收集器。不幸地是,來自 groupingBy 的結果類型幾乎是不可讀的!對于這一點你毫無辦法,你能做的隻有觀察。

最重要的一點是當 Java 中增加新的類庫的時候,他們開發越來越多的泛型,這就為開發者引進了更多的公式化代碼(boilerplate code),進而帶來了額外的壓力。上面的例子并不是說明了編寫類型就不好。很明顯,強制将為變量和方法簽名定義類型的操作執行為一種需要被尊重的協定,将有益于維護和了解。然而,為中間表達式聲明類型也許會顯得無用和備援。

類型推斷的曆史

我們已經在 Java 曆史上多次看到語言設計者添加“類型推斷”來幫助我們編寫更簡潔的代碼。類型推斷是一種思想:編譯器可以幫你推出靜态類型,你不必自己指定它們。

最早從 Java 5 開始就引入了泛型方法,而泛型方法的參數可以通過上下文推導出來。比如

這段代碼:

Java 10 局部變量類型推斷是什麼?

可以簡化成:

Java 10 局部變量類型推斷是什麼?

然後,在 Java 7 中,可以在表達式中省略類型參數,隻要這些參數能通過上下文确定。比如:

Java 10 局部變量類型推斷是什麼?

可以使用尖括号<>運算符簡化成:

Java 10 局部變量類型推斷是什麼?

一般來說,編譯器可以根據周圍的上下文來推斷類型。在這個示例中,從左側可以推斷出 HashMap 包含字元串清單。

從 Java 8 開始,像下面這樣的 Lambda 表達式

Java 10 局部變量類型推斷是什麼?

可以省略類型,寫成

Java 10 局部變量類型推斷是什麼?

局部變量類型推斷

随着類型越來越多,泛型參數有可能是另一個泛型,這種情況下類型推導可以增強可讀性。Scala 和 C# 語言允許将局部變量的類型聲明為 var,由編譯器根據初始化語句來填補合适的類型。比如,前面對 userChannels 的聲明可以寫成這樣:

Java 10 局部變量類型推斷是什麼?

也可以是根據方法的傳回值(這裡傳回清單)來推斷:

Java 10 局部變量類型推斷是什麼?

這種思想稱為局部變量類型推斷,它已經在 Java 10 中引入!

例如下面的代碼:

Java 10 局部變量類型推斷是什麼?

在 Java 10 中可以重構成這樣:

Java 10 局部變量類型推斷是什麼?

上述代碼中的每個表達式仍然是靜态類型(即值的類型):

局部變量 path 的類型是 Path

變量 lines 的類型是 Stream<String>

變量 warningCount 的類型是 long

也就是說,如果給這些變量賦予不同值則會失敗。比如,像下面這樣的二次指派會造成編譯錯誤:

Java 10 局部變量類型推斷是什麼?

然而還有一些關于類型推斷的小問題;如果類 Car 和 Bike 都是 Vehicle 的子類,然後聲明

Java 10 局部變量類型推斷是什麼?

這裡聲明的 v 的類型是 Car 還是 Vehicle?這種情況下很好解釋,因為初始化器(這裡是 Car)的類型非常明确。如果沒有初始化器,就不能使用 var。稍後像這樣指派會出錯。

Java 10 局部變量類型推斷是什麼?

換句話說,var 并不能完美地應用于多态代碼。

那應該在哪裡使用局部變量類型推斷呢?

什麼情況下局部類型推斷會失效?你不能在字段和方法簽名中使用它。它隻能用于局部變量,比如下面的代碼是不正确的:

Java 10 局部變量類型推斷是什麼?

不能在不明确初始化變量的情況下使用 var 聲明局部變量。也就是說,不能使用 var 文法聲明一個沒有指派的變量。下面這段代碼

Java 10 局部變量類型推斷是什麼?

這會産生編譯錯誤:

Java 10 局部變量類型推斷是什麼?

也不能把 var 聲明的變量初始化為 null。實事上,在後期初始化之前它究竟是什麼類型,這并不清楚。

Java 10 局部變量類型推斷是什麼?

不能在 Lambda 表達式中使用 var,因為它需要明确的目标類型。下面的指派就是錯的:

Java 10 局部變量類型推斷是什麼?

但是,下面的指派卻是有效的,原因是等式右邊确實有一個明确的初始化。

Java 10 局部變量類型推斷是什麼?

這個清單的靜态類型是什麼?變量的類型被推導為 ArrayList<Object>,這完全失去了泛型的意義,是以你可能會想避免這種情況。

對無法表示的類型進行推斷

Java 中存在大量無法表示的類型——這些類型存在于程式中,但是卻不能準确地寫出其名稱。比如匿名類就是典型的無法表示的(Non-Denotable Types)類型,你可以在匿名類中添加字段和方法,但你沒辦法在 Java 代碼中寫出匿名類的名稱。尖括号運算符不能用于匿名類,而var 受到的限制會稍微少一些,它可以支援一些無法表示的類型,詳細點說就是匿名類和交叉類型。

var 關鍵字也能讓我們更有效地使用匿名類,它可以引用那些不可描述的類型。一般來說是可以在匿名類中添加字段的,但是你不能在别的地方引用這些字段,因為它需要變量在指派時指定類型的名稱。比如下面這段代碼就不能通過編譯,因為 productInfo 的類型是 Object,你不能通過 Object 類型來通路 name 和 total 字段。

Java 10 局部變量類型推斷是什麼?

使用 var 可以打破這個限制。把一個匿名類對象指派給以 var 聲明的局部變量時,它會推斷出匿名類的類型,而不是把它當作其父類類型。是以,匿名類上聲明的字段就可以引用到。

Java 10 局部變量類型推斷是什麼?

乍一看這隻是語言中比較有趣的東西,并不會有太大用處。但在某些情況下它确實有用。比如你想傳回一些值作為中間結果的時候。一般來說,你會為此建立并維護一個新的類,但隻會在一個方法中使用它。在 Collectors.averagingDouble() 的實作中就因為這個原因,使用了一個 double 類型的小數組。

有了 var 之後我們就有了更好的處理辦法 - 用匿名類來儲存中間值。現在來思考一個例子,有一些産品,每個都有名稱、庫存和貨币價值或價值。我們要計算計算每一項的總價(數量*價值)。這些是我們要将每個 Product 映射到其總價所需要的資訊,但是為了讓資訊更有意義,還需要加入産品的名稱。下面的示例描述了在 Java 10 中如何使用 var 來實作這一功能:

Java 10 局部變量類型推斷是什麼?

并非所有無法表示的類型都可以用 var - 它隻支援匿名類和交叉類型。由通配符比對的類型就不能被推斷,這會避免與通配符相關的錯誤被報告給 Java 程式員。支援無法表示的類型的目的是在推斷類型中盡量保留更多資訊,讓人們可以利用局部變量并更好地重構代碼。

這一特性的初衷并不是要人們像上面的示例中那樣編寫代碼,而是為了使用 var 簡化處理無法表示類型相關的一些問題。以後是否會使用 var 來處理無法表示的類型的一些細節問題,尚不可知。

類型推斷建議

類型推斷确實有助于快速編寫 Java 代碼,但是可讀性如何呢?開發者大約會花 10 倍于寫代碼的時候來閱讀代碼,是以應該讓代碼更易讀而不是更易寫。var 對此帶來的改善程度總是主觀評價的,不可避免地會有人喜歡它,也會有人讨厭它。你應該關注的是如何幫助團隊成員閱讀你的代碼,是以如果他們喜歡閱讀使用 var 的代碼,那就用,不然就不用。

有時候,顯示類型也會降低可讀性。比如,在循環周遊 Map 的 entryset 時,你需要找到 Map.Entry 對象的類型參數。這裡有一個周遊 Map 的示例,這個 Map 将國家名稱映射到其中的城市名稱清單。

Java 10 局部變量類型推斷是什麼?

然後用 var 來重寫這段代碼,減少重複和繁瑣的東西:

Java 10 局部變量類型推斷是什麼?

這裡不僅帶來了可讀性方面的優勢,在改進和維護代碼方面也帶來了優勢。如果我們在顯式類型的代碼中将城市從 String 表示的名稱改為 City 類,以保留更多城市資訊,那就需要重寫所有依賴于特定類型的代碼,比如:

Java 10 局部變量類型推斷是什麼?

但使用了 var 關鍵字和類型推導,我們就隻需要修改第一行代碼就好:

Java 10 局部變量類型推斷是什麼?

這說明了一個使用 var 變量的重要原則:不要為了易于編碼而優化,也不要為了易讀而優化,而要了易維護性而優化。同時要考慮部分代碼可能以後會修改而要折衷考慮代碼的可讀性。

當然如果說添加類型推斷對代碼隻會有好處略顯武斷,有時明确的類型有助于代碼可讀性。特别是當某些生成的表達式類型不是很直覺時,我将選擇顯式而不是隐式類型,比如從下邊的代碼中我并不能看出 getCitiest() 方法會傳回什麼對象:

Java 10 局部變量類型推斷是什麼?

既然要同時考慮到可讀性和 var ,那麼如何折衷就成了一個新問題,一個建議是:關注變量名,這很重要!因為 var 失去代碼的易讀性,看到這樣的代碼你根本不知道代碼的意圖是什麼,這就使得起好一個變量名更加重要。理論上這是JAVA程式員應努力的方面之一,實際上許多 Java 代碼可讀性的問題根本不在語言的特性本身,而存在于一些變量的命名不太恰當上。

IDE 中的類型推斷

許多 IDE 都有提取局部變量的功能,它們可以正确地推斷出變量的類型,并為你寫出來。這一特性與 Java 10 的 var 有一些重複。IDE 的這個特性和 var 一樣都可以消除顯式書寫類型的必要性,但是它們在其它方面有一些不同。

局部提取功能會在代碼中生成完整的、類型明确的局部變量。而 var 則是消除了在代碼寫顯式書寫類型的必要。是以雖然他們在簡化書寫代碼方面有着類似的作用,但 var 對代碼可讀性的影響是局部提取功能所不具備的。就像我們前面提到,它多數時候會提高可讀性,但有時候會可能會降低可讀性。

與其它程式設計語言比較

Java 并不是首先實作變量類型推斷的語言。類型推斷在近幾十年來被廣泛應用于其它語言中。實際上,Java 10 中通過 var 帶來的類型推斷非常有限,形式上也相對拘束。這是一種簡單的實作,可以将與 var 聲明相關的編譯錯誤限制在一條語句當中,因為 var 推斷算法隻需要計算指派給變量的表達式的類型。此外,用在大多數語言中的 Hindley-Milner 類型推斷算法在最壞的情況下會花費指數級時間,這會降低 javac 的速度。

總結