JDK 9-17新功能30分鐘詳解-語法篇-var
介紹
JDK 10
JDK 10新增了新的關鍵字——var,官方檔案說作用是:
Enhance the Java Language to extend type inference to declarations of local variables with initializers
大體意思就是用于帶有初始化的區域變數宣告,廢話不多說,我們直接用具體代碼來展示實際的作用,
List<String> listBefore10 = new ArrayList<>(); # 在JDK10之前
var listAfter10 = new ArrayList<String>(); # 在JDK10之后
listBefore10.add("9");
listAfter10.add("10");
JDK 11
JDK 11對var做了調整,允許var關鍵字用于Lambda函式里面的引數型別宣告,示例:
var result = Arrays.asList("Java", "11").stream().reduce((var x, var y) -> x + y);
System.out.println(result.orElseThrow());
原理
可以看到使用了var關鍵字后,節省了一點宣告內容,但是仔細一看,例如一個泛型型別從宣告部分,挪到了初始化部分去了,我們直接看反編譯后的class檔案:

可以看到,其實var關鍵字對于我們來說就是一個語法糖,編譯完成后var宣告的變數型別已經確定下來了,實際運行的時候是無法起到類似于Javascript語言var宣告變數后還能動態更換型別的效果,至于為什么使用必須同時宣告和初始化的方式,而不是先宣告,后初始化再進行型別推斷的方式,官方大體是基于下面考慮的
The majority (more than 75% in both JDK and broader corpus) of local variables with initializers were already effectively immutable anyway, meaning that any "nudge" away from mutability that this feature could have provided would have been limited.
超過75%的JDK庫及其相關擴展中,帶有初始化的區域變數,都是有效不可變的,即使提供了延后初始化功能起到的作用也不大,
We chose the restriction ... because it covers a significant fraction of the candidates while maintaining the simplicity of the feature and reducing "action at a distance" errors.
使用這種方式既能覆寫絕大數使用場景,又能保持功能簡潔,另外一方面也是為了減少可能存在的維護問題,理解的心智成本,例如宣告后經過幾百行的代碼再進行初始化,
具體內容感興趣的可以看下JEPS 286的Scope Choices部分,
限制
1. 必須初始化
var原理大抵是編譯器通過初始化的值推斷宣告的型別,由此引出使用它的一個約束——宣告的同時必須進行初始化,
# 錯誤示例
var listAfter10;
listAfter10 = new ArrayList<String>();
listAfter10.add("10");
用以上代碼直接編譯運行,JDK會報錯,提示:
java: 無法推斷本地變數 listAfter10 的型別
(無法在不帶初始化程式的變數上使用 'var')
如果使用IDE,都不用運行就會直接提示你,例如Intellij IDEA:

Cannot infer type:'var' on variable without initializer
回看之前說到的官方宣告,“type inference to declarations of local variables with initializers”,with initializers已經很好說明使用它必須初始化,否則編譯器無法進行型別推斷,
2. 不能為null值
雖然進行初始化,但是使用null值的話,編譯器仍然無法進行型別推斷確定你最終的型別,也會報錯,

Cannot infer type:variable initializer is 'null'
3. 不能用于非區域變數
回看之前說到的官方宣告,“type inference to declarations of local variables with initializers”,local variable只能用于區域變數的使用,全域變數或者物件屬性宣告都不行,例如下面示例是無法正常運行:
# 錯誤示例
public class Java10 {
public var field = "Not allow here";
}
編譯直接報錯
此處不允許使用 'var'
4. 不能用于Lambda運算式型別的宣告
編譯器不支持推斷匿名函式的型別,例如:
# 錯誤示例
var lambdaVar = (String s) -> s != null && s.length() > 0;

Cannot infer type:lambda expression requires an explicit target type
編譯直接報錯
java: 無法推斷本地變數 lambdaVar 的型別
(lambda 運算式需要顯式目標型別)
但是這樣使用是可以的:
# 正確示例
var lambdaVar = (Function<String, Boolean>) (String s) -> s != null && s.length() > 0;
不過這樣寫就是脫褲子放屁了,直接寫在前面宣告不是更好,
亦或者雖然使用了匿名函式,但是其回傳值并不是一個Lambda運算式型別,也是可以的,
# 正確示例
var result = Arrays.asList("Java", "10").stream().reduce((x, y) -> x + y);
5. Lambda函式var修飾引數不能與其他型別混合使用
# 錯誤示例
var result = Arrays.asList("Java", "11").stream().reduce((var x, y) -> x + y);
System.out.println(result.orElseThrow());
# 錯誤示例
var result = Arrays.asList("Java", "11").stream().reduce((var x, String y) -> x + y);
System.out.println(result.orElseThrow());
就是同一個匿名方法里面要不就都是var修飾,要不就都不用,不能一個用,另外一個不用這種混合使用,當然官方說理論上是可行的,但是由于超出本次JEP規范定義,所以保留這些限制條件,
In theory, it would be possible to have a lambda expression like the last line above, which is semi-explicitly typed (or semi-implicitly typed, depending on your point of view). However, it is outside the scope of this JEP because it deeply affects type inference and overload resolution.This is the main reason for keeping the restriction that a lambda expression must specify all manifest parameter types or none.
使用規范
使用var帶來的好處是簡化了開發者的區域變數宣告成本,但是同時也可能造成代碼維護上的不便,特別是開發者和維護者不是同一個人的情況,為此官方也出了一版7個小點的var使用規范,
1. 使用有意義的變數名
# 不規范示例
List<Customer> x = dbconn.executeQuery(query);
# 正確示例
var custList = dbconn.executeQuery(query);
2. 區域變數使用范圍盡可能地小
# 不規范示例
var items = new HashSet<Item>(...);
// ... 中間大概隔了幾百行的代碼 ...
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) {
...
}
一個方法行數過多,本身已經不利于維護,再加上使用var修飾變數,維護的人可能要滑鼠滾動一屏甚至幾屏才能看到var變數的具體使用,理解成本大大提高,所以一般情況下var變數保持在一屏內使用就好,
3. 初始化部分有意義時可以使用
var outputStream = new ByteArrayOutputStream();
var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c");
初始化的部分,例如呼叫的方法名稱或者構造型別名字簡單易懂,可以直接使用,
4. 用于拆分鏈式呼叫或者嵌套呼叫
return "test string".stream()
.collect(groupingBy(s -> s, counting()))
.entrySet()
.stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey);
上面的鏈式呼叫不方便理解或者除錯,可以改為
Map<String, Long> freqMap = "test string".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 freqMap = "test string".stream().collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet().stream().max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);
5. 不用顧慮用于面向介面開發
List<String> normalList = new ArrayList<>();
var varList = new ArrayList<String>(); # varList最終推斷型別是ArrayList<String>而不是List<String>
由于var只能用于區域變數,對于面向介面開發的原則基本無影響,問題主要是var初始化部分的型別依賴,如果發生變化,例如上面示例的ArrayList改成LinkedList,varList的型別隨之變化,但是如果遵循規范“2. 區域變數使用范圍盡可能地小”的話,影響面就會比較小,
6. 謹慎用于泛型型別
# 正確示例
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();
# 不規范示例
var itemQueue2 = new PriorityQueue<>(); # itemQueue2最終推斷型別是PriorityQueue<Object>
可能導致型別推斷的最終型別不是想要的泛型型別,
7. 謹慎用于字面量
byte flags = 0;
short mask = 0x7fff;
long base = 17;
改成
var flags = 0;
var mask = 0x7fff;
var base = 17;
全部型別都會推導為int,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/502094.html
標籤:Java
上一篇:樹基本概念及用法
下一篇:深入理解java泛型
