簡介
Java SE 10引入了局部變量的類型推斷。早先,所有的局部變量聲明都要在左側聲明明確類型。 使用類型推斷,一些顯式類型可以替換為具有初始化值的局部變量保留類型var,這種作為局部變量類型 的var類型,是從初始化值的類型中推斷出來的。
關於此功能存在一定的爭議。有些人對它的簡潔性表示歡迎,其他人則擔心它剝奪了閱讀者看重的類型信息 ,從而損害了代碼的可讀性。這兩邊觀點都是對的。它可以通過消除冗余信息使代碼更具有可讀性,也可以 通過刪除有用的信息來降低代碼的可讀性。另外一個觀點是擔心它會被濫用,從而導致編寫更糟糕的Java代碼。 這也是事實,但它也可能會導致編寫更好的代碼。像所有功能一樣,使用它必須要判斷。何時該使用, 何時不該使用,沒有一系列規則。
局部變量聲明不是孤立存在的;周邊的代碼可以影響甚至壓倒使用var的影響。本文檔的目的是檢查周邊代碼 對var聲明的影響,解釋一些權衡,並提供有效使用var的指南。
使用原則
P1. 閱讀代碼比編寫代碼更重要
代碼的讀取頻率遠高於編寫代碼。此外,在編寫代碼時,我們通常要有整體思路,並且要花費時間; 在閱讀代碼的時候,我們經常是上下文瀏覽,而且可能更加匆忙。是否以及如何使用特定語言功能應該取決 於它對未來讀者的影響,而不是它的作者。較短的程序可能比較長的程序更可取,但是過多地簡寫程序會 省略對於理解程序有用的信息。這里的核心問題是為程序找到合適的大小,以便最大化程序的可讀性。
我們在這里特別關注在輸入或編寫程序時所需要的碼字量。雖然簡潔性可能會是對作者的一個很好的鼓勵,但 專注於它會忽略主要的目標,即提高程序的可讀性。
P2. 在經過局部變量類型推斷后,代碼應該變得清晰
讀者應該能夠查看var聲明,並且使用局部變量聲明的時候可以立刻了解代碼里正在發生了什么事情。理想 情況下,只通過代碼片段或上下文就可以輕松理解代碼。如果讀懂一個var聲明需要讀者去查看代碼周邊的 若干位置代碼,此時使用var可能不是一個好的情況。而且,它還表明代碼本身可能存在問題。
P3. 代碼的可讀性不應該依賴於IDE
代碼通常在IDE中編寫和讀取,所以很容易依賴IDE的代碼分析功能。對於類型聲明,在任何地方使用var,都可以通過IDE指向一個 變量來確定它的類型,但是為什么不這么做呢?
這里有兩個原因。代碼經常在IDE外部讀取。代碼出現在IDE設施不可用的許多地方,例如文檔中的片段,瀏覽互聯網上的倉庫或補丁文件 。為了理解這些代碼的作用,你必須將代碼導入IDE。這樣做是適得其反的。
第二個原因是即使是在IDE中讀取代碼時也是如此,通常需要明確的操作來查詢IDE以獲取有關變量的更多信息。例如,查詢使用var聲明 的變量類型,可能必須將鼠標懸停在變量上並等待彈出信息,這可能只需要片刻時間,但是它會擾亂閱讀流程。
代碼應該是自動呈現的,它的表面應該是可以理解的,並且無需工具的幫助。
P4. 顯式類型是一種權衡
Java歷來要求局部變量聲明里要明確包含顯式類型,顯然顯式類型可能非常有用,但它們有時候不是很重要,有時候還可以忽略。要求一個 明確的類型可能還會混亂一些有用的信息。
省略顯式類型可以減少這種混亂,但只有在這種混亂不會損害其可理解性的情況下。這種類型不是向讀者傳達信息的唯一方式。其他方法 包括變量的名稱和初始化表達式也可以傳達信息。在確定是否可以將其中一個頻道靜音時,我們應該考慮所有可用的頻道。
指南
G1. 選擇能夠提供有用信息的變量名稱
通常這是一個好習慣,不過在var的背景下它更為重要。在一個var的聲明中,可以使用變量的名稱來傳達有關變量含義和用法的信息。 使用var替換顯式類型的同時也要改進變量的名稱。例如:
//原始寫法
List<Customer> x = dbconn.executeQuery(query);
//改進寫法
var custList = dbconn.executeQuery(query);
在這種情況下,無用的變量名x已被替換為一個能夠喚起變量類型的名稱custList,該名稱現在隱含在var的聲明中。 根據方法的邏輯結果對變量的類型進行編碼,得出了”匈牙利表示法”形式的變量名custList。 就像顯式類型一樣,這樣有時是有幫助的,有時候只是雜亂無章。在此示例中,名稱custList表示正在返回List。這也可能不重要。 和顯式類型不同,變量的名稱有時可以更好地表達變量的角色或性質,比如”customers”:
//原始寫法
try (Stream<Customer> result = dbconn.executeQuery(query)) {
return result.map(...)
.filter(...)
.findAny();
}
//改進寫法
try (var customers = dbconn.executeQuery(query)) {
return customers.map(...)
.filter(...)
.findAny();
}
G2.最小化局部變量的使用范圍
最小化局部變量的范圍通常也是一個好的習慣。這種做法在 Effective Java (第三版),第57項 中有所描述。 如果你要使用var,它會是一個額外的助力。
在下面的例子中,add方法正確地將特殊項添加到list集合的最后一個元素,所以它按照預期最后處理。
var items = new ArrayList<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...
現在假設為了刪除重復的項目,程序員要修改此代碼以使用HashSet而不是ArrayList:
var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...
這段代碼現在有個bug,因為Set沒有定義迭代順序。不過,程序員可能會立即修復這個bug,因為items變量的使用與其聲明相鄰。
現在假設這段代碼是一個大方法的一部分,相應地items變量具有很大的使用范圍:
var items = new HashSet<Item>(...);
// ... 100 lines of code ...
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...
將ArrayList更改為HashSet的影響不再明顯,因為使用items的代碼與聲明items的代碼離得很遠。這意味着上面所說的bug似乎可以存活 更長時間。
如果items已經明確聲明為List,還需要更改初始化程序將類型更改為Set。這可能會提示程序員檢查方法的其余部分 是否存在受此類更改影響的代碼。(問題來了,他也可能不會檢查)。如果使用var會消除這類影響,不過也可能會增加在此類代碼中 引入錯誤的風險。
這似乎是反對使用var的論據,但實際上並非如此。使用var的初始化程序非常精簡。僅當變量的使用范圍很大時才會出現此問題。 你應該更改代碼來減少局部變量的使用范圍,然后用var聲明它們,而不是簡單地避免在這些情況下使用var。
G3. 當初始化程序為讀者提供足夠的信息時,請考慮使用var
局部變量通常在構造函數中進行初始化。正在創建的構造函數名稱通常與左側顯式聲明的類型重復。 如果類型名稱很長,就可以使用var提升簡潔性而不會丟失信息:
// 原始寫法:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 改進寫法
var outputStream = new ByteArrayOutputStream();
在初始化程序是方法調用的情況下,使用var也是合理的,例如靜態工廠方法,並且其名稱包含足夠的類型信息:
//原始寫法
BufferedReader reader = Files.newBufferedReader(...);
List<String> stringList = List.of("a", "b", "c");
// 改進寫法
var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c");
在這些情況下,方法的名稱強烈暗示其特定的返回類型,然后用於推斷變量的類型。
G4. 使用var局部變量分解鏈式或嵌套表達式
考慮使用字符串集合並查找最常出現的字符串的代碼,可能如下所示:
return strings.stream()
.collect(groupingBy(s -> s, counting()))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
這段代碼是正確的,但它可能令人困惑,因為它看起來像是一個單一的流管道。事實上,它先是一個短暫的流,接着是第一個流的結果生成 的第二個流,然后是第二個流的可選結果映射后的流。表達此代碼的最易讀的方式是兩個或三個語句; 第一組實體放入一個Map,然后第二組過濾這個Map,第三組從Map結果中提取出Key,如下所示:
Map<String, Long> freqMap = strings.stream()
.collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
.stream()
.max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);
但編寫者可能會拒絕這樣做,因為編寫中間變量的類型似乎太過於繁瑣,相反他們篡改了控制流程。使用var允許我們更自然地表達代碼 ,而無需付出顯式聲明中間變量類型的高代價:
var freqMap = strings.stream()
.collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet()
.stream()
.max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);
有些人可能更傾向於第一段代碼中單個長的鏈式調用。但是,在某些條件下,最好分解長的方法鏈。對這些情況使用var是一種可行的 方法,而在第二個段中使用中間變量的完整聲明會是一個不好的選擇。 和許多其他情況一樣,正確使用var可能會涉及到扔掉一些東西 (顯示類型)和加入一些東西(更好的變量名稱,更好的代碼結構)。
G5. 不用過分擔心”使用接口編程” 中局部變量的使用問題
Java編程中常見的習慣用法是構造具體類型的實例,但要將其分配給接口類型的變量。這將代碼綁定到抽象上而不是具體實現上,為 代碼以后的維護保留了靈活性。
//原始寫法, list類型為接口List類型
List<String> list = new ArrayList<>()
如果使用var,可以推斷出list具體的實現類型ArrayList而不是接口類型List
// 推斷出list的類型是 ArrayList<String>.
var list = new ArrayList<String>();
這里要再次重申一次,var只能用於局部變量。它不能用於推斷屬性類型,方法參數類型和方法返回類型。”使用接口編程”的原則在這些 情況下仍然和以往一樣重要。
主要問題是使用該變量的代碼可以形成對具體實現的依賴性。如果變量的初始化程序以后要改變,這可能導致其推斷類型發生變化,在 使用該變量的后續代碼中產生異常或bug。
如果,如指南G2中所建議的那樣,局部變量的范圍很小,可能影響后續代碼的具體實現的”漏洞”是有限的。如果變量僅用於幾行之外的 代碼,應該很容易避免這些問題或者緩解這些出現的問題。
在這種特殊情況下,ArrayList只包含一些不在List上的方法,如ensureCapacity()和trimToSize()。這些方法不會影響到List,所以 調用他們不會影響程序的正確性。這進一步降低了推斷類型作為具體實現類型而不是接口類型的影響。
G6. 使用帶有<>和泛型方法的var時候要小心
var和<>功能允許您在可以從已存在的信息派生時,省略具體的類型信息。你能在同一個變量聲明中使用它們嗎?
考慮一下以下代碼:
PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
這段代碼可以使用var或<>重寫,並且不會丟失類型信息:
// 正確:兩個變量都可以聲明為PriorityQueue<Item>類型
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();
同時使用var和<>是合法的,但推斷類型將會改變:
// 危險: 推斷類型變成了 PriorityQueue<Object>
var itemQueue = new PriorityQueue<>();
從上面的推斷結果來看,<>可以使用目標類型(通常在聲明的左側)或構造函數作為<>里的參數類型。如果兩者都不存在,它會追溯到 最寬泛的適用類型,通常是Object。這通常不是我們預期的結果。
泛型方法早已經提供了類型推斷,使用泛型方法很少需要提供顯式類型參數,如果是沒有提供足夠類型信息的實際方法參數,泛型方法 的推斷就會依賴於目標類型。在var聲明中沒有目標類型,所以也會出現類似的問題。例如:
// 危險: list推斷為 List<Object>
var list = List.of();
使用<>和泛型方法,可以通過構造函數或方法的實際參數提供其他類型信息,允許推斷出預期的類型,從而有:
// 正確: itemQueue 推斷為 PriorityQueue<String>
Comparator<String> comp = ... ;
var itemQueue = new PriorityQueue<>(comp);
// 正確: infers 推斷為 List<BigInteger>
var list = List.of(BigInteger.ZERO);
如果你想要將var與<>或泛型方法一起使用,你應該確保方法或構造函數參數能夠提供足夠的類型信息,以便推斷的類型與你想要的類型匹配。 否則,請避免在同一聲明中同時使用var和<>或泛型方法。
G7.使用基本類型的var要小心
基本類型可以使用var聲明進行初始化。在這些情況下使用var不太可能提供很多優勢,因為類型名稱通常很短。不過,var有時候也很有用 ,例如,可以使變量的名稱對齊。
boolean,character,long,string的基本類型使用var沒有問題,這些類型的推斷是精確的,因為var的含義是明確的
// 原始做法
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";
// 改進做法
var ready = true;
var ch = '\ufffd';
var sum = 0L;
var label = "wombat";
當初始值是數字時,應該特別小心,特別是int類型。如果左側有顯示類型,那么右側會通過向上或向下轉型將int數值默默轉為 左邊對應的類型。如果左邊使用var,那么右邊的值會被推斷為int類型。這可能是無意的。
// 原始做法
byte flags = 0;
short mask = 0x7fff;
long base = 17;
// 危險: 所有的變量類型都是int
var flags = 0;
var mask = 0x7fff;
var base = 17;
如果初始值是浮點型,推斷的類型大多是明確的:
// 原始做法
float f = 1.0f;
double d = 2.0;
// 改進做法
var f = 1.0f;
var d = 2.0;
注意,float類型可以默默向上轉型為double類型。使用顯式的float變量(如3.0f)為double變量做初始化會有點遲鈍。不過, 如果是使用var對double變量用float變量做初始化,要注意:
// 原始做法
static final float INITIAL = 3.0f;
...
double temp = INITIAL;
// 危險: temp被推斷為float類型了
var temp = INITIAL;
(實際上,這個例子違反了G3准則,因為初始化程序里沒有提供足夠的類型信息能讓讀者明白其推斷類型。)
示例
本節包含一些示例,這些例子中使用var可以收到良好的效果。
下面這段代碼表示的是根據最多匹配數max從一個Map中移除匹配的實體。通配符(?)類型邊界可以提高方法的靈活性,但是長度會很長 。不幸的是,這里Iterator的類型還被要求是一個嵌套的通配符類型,這樣使它的聲明更加的冗長,以至於for循環標題長度在一行里 都放不下。也使代碼更加的難懂。
// 原始做法
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
for (Iterator<? extends Map.Entry<? extends String, ? extends Number>> iterator =
map.entrySet().iterator(); iterator.hasNext();) {
Map.Entry<? extends String, ? extends Number> entry = iterator.next();
if (max > 0 && matches(entry)) {
iterator.remove();
max--;
}
}
}
在這里使用var就可以刪除掉局部變量一些干擾的類型聲明。在這種循環中顯式的類型Iterator,Map.Entry在很大程度上是沒有必要的,
使用var聲明就能夠讓for循環標題放在同一行。代碼也更易懂。
// 改進做法
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
for (var iterator = map.entrySet().iterator(); iterator.hasNext();) {
var entry = iterator.next();
if (max > 0 && matches(entry)) {
iterator.remove();
max--;
}
}
}
考慮使用try-with-resources語句從Socket讀取單行文本的代碼。網絡和IO類一般都是裝飾類。在使用時,必須將每個中間對象聲明為 資源變量,以便在打開后續的裝飾類的時候能夠正確關閉這個資源。常規編寫代碼要求在變量左右聲明重復的類名,這樣會導致很多混亂:
// 原始做法
try (InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is, charsetName);
BufferedReader buf = new BufferedReader(isr)) {
return buf.readLine();
}
使用var聲明會減少很多這種混亂:
// 改進做法
try (var inputStream = socket.getInputStream();
var reader = new InputStreamReader(inputStream, charsetName);
var bufReader = new BufferedReader(reader)) {
return bufReader.readLine();
}
結語
使用var聲明可以通過減少混亂來改善代碼,從而讓更重要的信息脫穎而出。另一方面,不加選擇地使用var也會讓事情變得更糟。使用 得當,var有助於改善良好的代碼,使其更短更清晰,同時又不影響可理解性。
私信回復 “資料” 領取一線大廠Java面試題總結+阿里巴巴泰山手冊+各知識點學習思維導+一份300頁pdf文檔的Java核心知識點總結!
這些資料的內容都是面試時面試官必問的知識點,篇章包括了很多知識點,其中包括了有基礎知識、Java集合、JVM、多線程並發、spring原理、微服務、Netty 與RPC 、Kafka、日記、設計模式、Java算法、數據庫、Zookeeper、分布式緩存、數據結構等等。