簡介
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、分布式快取、資料結構等等,

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/107429.html
標籤:Java
上一篇:Maven核心概述
下一篇:IDEA中配置Maven
