【原創】JDK 9-17新功能30分鐘詳解-語法篇-var
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。