使用Java 10的var型別推斷的幾個注意點! - DZone Java

banq發表於2019-05-16

不加選擇地應用var可能會讓程式碼不容易理解,因為模糊了型別這個概念,而人類是依據型別分類進行邏輯思考的,這樣就使事情變得更糟,如果使用得當,var可以幫助改進良好的程式碼,使其更短更清晰,同時不會影響可理解性。
使用var需要透過減少混亂來改進程式碼,從而使更重要的資訊脫穎而出。
本地型別推斷功能背後的主要前提非常簡單。使用新的保留型別名稱'var'替換宣告中的顯式型別,並推斷其型別。所以我們可以替換原來:

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();


為:

var outputStream = new ByteArrayOutputStream();


Java現在允許動態型別了嗎?絕對不!
所有型別推斷都在編譯時發生,顯式型別由編譯器烘焙到位元組程式碼中。在執行時,Java與以往一樣靜態。鑑於使用非常簡單,本備忘單將集中在本地型別推斷的最重要方面 - 它的實際用途。當您應該使用顯式型別以及何時應該考慮型別推斷時,它將提供指導。
由於想要編寫這個備忘單,Oracle的JDK工程師Stuart Marks了一篇完美的文章,給出了編碼原理和使用型別推理的指導,我將它們濃縮成一張備忘單:

原則
1.閱讀程式碼>編寫程式碼
無論是花10分鐘還是10天寫一行程式碼,你幾乎肯定會在未來的許多年裡閱讀它。如果程式碼清晰,簡潔,並且最重要的是包含了解其目的的所有必要資訊,那麼程式碼將來只能維護和理解。目標是最大化可理解性。

2.本地推理應明確程式碼
儘可能多地將資訊烘焙到程式碼中,以避免讀者必須檢視程式碼庫的不同部分,以便了解正在發生的事情。這可以透過方法或變數命名。

3.程式碼可讀性不應該依賴於IDE
IDE可以很棒。我的意思是真的很棒!它們可以使開發人員的開發更高效或更準確。程式碼必須易讀且易於理解,而不依賴於IDE。通常,程式碼在IDE外部讀取。或者,IDE可能會為讀者提供多少資訊。程式碼應該是自我暴露的。它應該是可以理解的,無需工具的幫助。

決定權在你
是否為變數提供顯式型別或讓Java編譯器為自己解決問題的選擇是一種權衡。一方面,你想減少雜亂,樣板,儀式。另一方面,您不希望損害程式碼的可理解性。型別宣告不是向讀者傳達資訊的唯一方式。其他方法包括變數的名稱和初始化表示式。

方法
1.選擇提供有用資訊的變數名稱
一般來說,這是一種很好的做法,但在var的上下文中它更為重要。在var宣告中,可以使用變數的名稱來傳達有關變數含義和用法的資訊。用var替換顯式型別通常應該伴隨著改進變數名。有時,在其名稱中對變數的型別進行編碼可能很有用。例如:

List<Customer> x = dbconn.executeQuery(query);
 var custList = dbconn.executeQuery(query);


2.最小化區域性變數的作用域
當變數的作用域很大時會發生此問題:這意味著變數宣告與其用法之間有許多程式碼行。隨著程式碼的維護,對型別的更改等可能最終會產生不同的行為。例如,從List移動​​到Set可能看起來沒問題,但是您的程式碼是否依賴於稍後在同一範圍內的排序?雖然型別總是靜態設定,但使用相同介面的實現中的細微差別可能會讓您失望。應該更改程式碼以減少區域性變數的作用域,然後用var宣告它們,而不是簡單地避免在這些情況下使用var。請考慮以下程式碼:

var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }


此程式碼現在有一個bug,因為集合沒有定義的迭代順序。但是,程式設計師可能會立即修復此錯誤,因為items變數的使用與其宣告相鄰。現在,假設此程式碼是大型方法的一部分,而items變數的作用域相應較大:

var items = new HashSet<Item>(...);
// ... 100 lines of code ...
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) { ... }


這個bug現在變得更難以追蹤,因為該行試圖將一個專案新增到集合的末尾並不足夠接近型別宣告以使該bug明顯。

3.初始化程式為Reader提供足夠的資訊時,請考慮Var
區域性變數通常用建構函式初始化。正在構造的類的名稱通常作為左側的顯式型別顯得累贅重複,如果型別名稱很長,則使用var可以提供簡潔而不會丟失資訊:

ByteArrayOutputStream  outputStream  =  new  ByteArrayOutputStream();
 var  outputStream  =  new  ByteArrayOutputStream();



4.使用Var分解具有區域性變數的連結或巢狀表示式
看看採用字串集合並查詢最常出現的字串的程式碼。這可能如下所示:

return strings.stream()
              .collect(groupingBy(s -> s, counting()))
              .entrySet()
              .stream()
              .max(Map.Entry.comparingByValue())
              .map(Map.Entry::getKey);


此程式碼是正確的,但在多個語句中更易讀。拆分語句的問題如下所示:

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



有人可能合法地更喜歡第一個片段及其單個長鏈方法呼叫。但是,在某些情況下,最好分解長方法鏈。

5.不要擔心使用區域性變數導致“程式設計介面”太多
Java程式設計中常見的習慣用法是構造具體型別的例項,但要將其分配給介面型別的變數。例如:

List<String> list = new ArrayList<>();


但是,如果使用var,則推斷出具體型別而不是介面:

// Inferred type of list is ArrayList<String>.
 var list = new ArrayList<String>();


使用list變數的程式碼現在可以形成對具體實現的依賴性。如果變數的初始化程式將來要更改,這可能會導致其推斷型別發生更改,從而導致在使用該變數的後續程式碼中發生錯誤或錯誤。
當遵守準則2時這不是問題,因為區域性變數的範圍很小,可能影響後續程式碼的具體實現的“洩漏”的風險是有限的。

6.使用泛型時要小心
var和泛型別功能允許您在可以從已存在的資訊派生時省略顯式型別資訊。但是,如果一起使用,它們可能最終會省略編譯器正確縮小您希望推斷的型別所需的所有有用資訊。

PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
 var itemQueue = new PriorityQueue<Item>();

//危險:推斷為PriorityQueue <Object>
 var itemQueue = new PriorityQueue<>();


泛型方法也成功地使用了型別推斷,程式設計師很少提供顯式型別引數。如果沒有提供足夠型別資訊的實際方法引數,則泛型方法的推斷依賴於目標型別。在var宣告中,沒有目標型別,因此可能會出現與diamond類似的問題。例如:

// DANGEROUS: infers as List<Object>
 var list = List.of();


使用泛型方法時,可以透過建構函式或方法的實際引數提供其他型別的資訊,從而允許推斷出預期的型別。這確實增加了額外的間接級別,但仍然是可預測的。從而:

// OK: itemQueue infers as PriorityQueue<String>
Comparator<String> comp = ... ;
 var itemQueue = new PriorityQueue<>(comp);


7.使用Var與文字Literals時要小心
使用帶文字的var不太可能提供許多優點,因為型別名稱通常很短。但是,var有時很有用,例如,對齊變數名稱。
布林值,字元,長字串和字串等文字沒有問題。從這些文字推斷出的型別是精確的,因此,var的含義是明確的。當初始值設定項是數值時,尤其是整數文字時,應特別小心。如果左側有顯式型別,則數值可以靜默加寬或縮小為int以外的型別。對於var,該值將被推斷為int,這可能是無意的。

// ORIGINAL
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";
byte flags = 0;
short mask = 0x7fff;
long base = 17;
 var ready = true;
 var ch    = '\ufffd';
 var sum   = 0L;
 var label = "wombat";
//危險:全部推斷為int
 var flags = 0;
 var mask = 0x7fff;
 var base = 17;


 

相關文章