譯:Java區域性變數型別推斷(Var型別)的26條細則
原文連結:https://dzone.com/articles/var-work-in-progress
作者:Anghel Leonard
譯者:沈歌
Java區域性變數型別推斷(LVTI),簡稱var
型別(識別符號var
不是一個關鍵字,是一個預留型別名),Java 10中通過JEP 286: Local-Variable Type Inference 新增進來。作為100%編譯特徵,它不會影響位元組碼,執行時或者效能。在編譯時,編譯器會檢查賦值語句右側程式碼,從而推斷出具體型別。它檢視宣告的右側,如果這是一個初始化語句,它會用那個型別取代var
。另外,它非常有助於減少冗餘程式碼和樣板程式碼。它還只在旨在編寫程式碼時所涉及的儀式。例如,使用var evenAndOdd=...
Map<Boolean, List<Integer>> evenAndOdd...
非常方便。根據用例,它有一個程式碼可讀性的權衡,會在下面第一條中提到。
此外,這裡有26條細則,覆蓋了var
型別的用例,包括它的限制。
1. 爭取起有意義的區域性變數名
通常我們在起全域性變數名的時候會注意這一點,但是選擇區域性變數名的時候不太注意。尤其是當方法很短,方法名和實現都不錯的時候,我們趨向於簡化我們的變數名。但是當我們使用var
替代顯式型別的時候,具體的型別是通過編譯器推斷出來的。所以,對於人來說閱讀或者理解程式碼非常困難。在這一點上var
削弱了程式碼可讀性。這種事情之所以會發生,是因為大多數情況下,我們會把變數型別當成是第一資訊,而把變數名當成第二資訊。但是使用var
示例1
即使到這裡,一些朋友仍然堅持區域性變數名短點好。我們看一下:
// HAVING
public boolean callDocumentationTask() {
DocumentationTool dtl = ToolProvider.getSystemDocumentationTool();
DocumentationTask dtt = dtl.getTask(...);
return dtt.call();
}
我們換成var
時,避免:
// AVOID public boolean callDocumentationTask() { var dtl = ToolProvider.getSystemDocumentationTool(); var dtt = dtl.getTask(...); return dtt.call(); }
更好:
// PREFER
public boolean callDocumentationTask() {
var documentationTool = ToolProvider.getSystemDocumentationTool();
var documentationTask = documentationTool.getTask(...);
return documentationTask.call();
}
示例2:
避免:
// AVOID
public List<Product> fetchProducts(long userId) {
var u = userRepository.findById(userId);
var p = u.getCart();
return p;
}
更好:
// PREFER
public List<Product> fetchProducts(long userId) {
var user = userRepository.findById(userId);
var productList = user.getCart();
return productList;
}
示例3:
爭取為區域性變數起有意義的名字並不意味著要掉入過度命名的坑,避免在短方法中使用單一型別的資料流:
// AVOID
var byteArrayOutputStream = new ByteArrayOutputStream();
用如下程式碼代替更加清晰:
// PREFER
var outputStream = new ByteArrayOutputStream();
// or
var outputStreamOfFoo = new ByteArrayOutputStream();
另外,你知道嗎,Java內部使用了一個類名字叫:InternalFrameInternalFrameTitlePaneInternalFrameTitlePaneMaximizeButtonWindowNotFocusedState
額。。。命名這個型別的變數是個挑戰。
2. 使用資料型別標誌來幫助var去推斷出預期的基本資料型別(int, long, float, double)
如果在基本資料型別中不使用有效的資料型別標誌,我們可能會發現預期的型別和推測出的型別不一致。這是由於var
的隱式型別轉換導致的。
例如,下面兩行程式碼的表現是符合預期的,首先,我們宣告一個boolean
和一個char
使用顯式型別:
boolean flag = true; // 這是一個boolean型別
char a = 'a'; // 這是一個char型別
現在,我們使用var
代替顯式基本型別:
var flag = true; // 被推斷為boolean型別
var a = 'a'; // 被推斷為char型別
到目前為止,一切都很完美。接下來,我們看一下相同邏輯下的int
, long
, double
和 float
:
int intNumber = 20; // 這是int型別
long longNumber = 20; // 這是long型別
float floatNumber = 20; // 這是float型別, 20.0
double doubleNumber = 20; // 這是double型別, 20.0
以上程式碼是很常見而且清晰的,現在我們使用var
:
避免:
// AVOID
var intNumber = 20; // 推斷為int
var longNumber = 20; // 推斷為int
var floatNumber = 20; // 推斷為int
var doubleNumber = 20; // 推斷為int
四個變數都被推斷成了int
。為了修正這個行為,我們需要依賴Java中的資料型別標誌。
更好實現:
// PREFER
var intNumber = 20; // 推斷為int
var longNumber = 20L; // 推斷為long
var floatNumber = 20F; // 推斷為float, 20.0
var doubleNumber = 20D; // 推斷為double, 20.0
但是如果我們使用小數宣告一個數字,會發生什麼呢?當你認為你的數字是一個float
的時候,避免這樣做:
// 避免,如果這是一個float
var floatNumber = 20.5; // 推斷為double
你應該用對應的資料型別標誌來避免這樣的問題:
// 更好, 如果這是一個float
var floatNumber = 20.5F; // 推斷為float
3. 在某些情況下,Var和隱式型別轉換可以維持可維護性
在某些情況下,Var和隱式型別轉換可以維持可維護性。例如,假設我們的程式碼包含兩個方法:第一個方法接收一個包含不同條目的購物卡,比較市場中不同的價格,計算出最好的價格,並彙總返回float
型別的總價。另一個方法簡單的把這個float
價格從卡中扣除。
首先,我們看一下計算最好價格的方法:
public float computeBestPrice(String[] items) {
...
float price = ...;
return price;
}
然後,我們看一下扣款的方法:
public boolean debitCard(float amount, ...) {
...
}
現在,我們把這兩個方法彙總,提供一個服務方法。顧客選擇要買的商品,計算最優價格,然後扣款:
// AVOID
public void purchaseCart(long customerId) {
...
float price = computeBestPrice(...);
debitCard(price, ...);
}
一段時間後,公司想要去除價格中的小數部分作為打折策略,使用int
代替了float
, 我們需要修改程式碼。
public int computeBestPrice(String[] items) {
...
float realprice = ...;
...
int price = (int) realprice;
return price;
}
public boolean debitCard(int amount, ...) {
...
}
問題在於我們使用了顯示型別float
,這樣的更改不能被相容。程式碼會報編譯時錯誤。但是如果我們預判到這種情況,使用var
代替float
, 我們的程式碼會因為隱式型別轉換而變得沒有相容性問題。
// PREFER
public void purchaseCart(long customerId) {
...
var price = computeBestPrice(...);
debitCard(price, ...);
}
4. 當資料型別標誌解決不了問題的時候,依賴顯式向下轉換或者避免var
一些Java基礎資料型別不支援資料型別標誌。例如byte
和short
。使用顯式基礎資料型別時沒有任何問題。使用var
代替的時候:
// 這樣更好,而不是使用var
byte byteNumber = 45; // 這是byte型別
short shortNumber = 4533; // 這是short型別
為什麼在這種情況下顯式型別比var
好呢?我們切換到var
.注意示例中都會被推斷為int
, 而不是我們預期的型別。
避免使用以下程式碼:
// AVOID
var byteNumber = 45; // 推斷為int
var shortNumber = 4533; // 推斷為int
這裡沒有基礎資料型別幫助我們,因此我們需要依賴顯示強制型別轉換。從個人角度來講,我會避免這麼用,因為沒啥好處,但是可以這麼用。
如果你真的想用var
,這麼用:
// 如果你真的想用var,這麼寫
var byteNumber = (byte) 45; // 推斷為byte
var shortNumber = (short) 4533; // 推斷為short
5. 如果變數名沒有對人來說足夠的型別資訊,避免使用var
使用var
有助於提供更加簡練的程式碼。例如, 在使用構造方法時(這是使用區域性變數的常見示例),我們可以簡單地避免重複類名的必要性,從而消除冗餘。
避免:
// AVOID
MemoryCacheImageInputStream inputStream = new MemoryCacheImageInputStream(...);
更好:
// PREFER
var inputStream = new MemoryCacheImageInputStream(...);
在下面的結構中,var
也是一個簡化程式碼而不丟失資訊的好方法。
避免:
// AVOID
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm = compiler.getStandardFileManager(...);
更好:
// PREFER
var compiler = ToolProvider.getSystemJavaCompiler();
var fileManager = compiler.getStandardFileManager(...);
為什麼這樣基於var
的例子我們感覺比較舒服呢?因為需要的資訊已經在變數名中了。但是如果使用var
加上變數名,還是會丟失資訊,那麼最好避免使用var
。
避免:
// AVOID
public File fetchCartContent() {
return new File(...);
}
// As a human, is hard to infer the "cart" type without
// inspecting the fetchCartContent() method
var cart = fetchCartContent();
使用以下程式碼代替:
// PREFER
public File fetchCartContent() {
return new File(...);
}
File cart = fetchCartContent();
思考一個基於java.nio.channels.Selector
的例子。這個類有一個靜態方法叫做open()
,返回一個新的Selector
例項並且執行open動作。但是Selector.open()
很容易被認為返回一個boolean
標識開啟當前選擇器是否成功,或者返回void
。使用var
導致丟失資訊會引發這樣的困擾。
6. var型別確保編譯時安全
var
型別是編譯時安全的。這意味著如果我們試圖實現一個錯的賦值,會導致編譯時報錯。例如,以下程式碼編譯不會通過。
// 編譯通不過
var items = 10;
items = "10 items"; // 不相容型別: String不能轉為int
以下程式碼編譯會通過
var items = 10;
items = 20;
這個程式碼也會編譯通過:
var items = "10";
items = "10 items" ;
所以,一旦編譯器已經推斷出了var
對應的型別,我們只能賦值對應型別的值給它。
7. var 不能被用於將真實型別的例項賦值給介面型別變數。
在Java中,我們使用“面向介面程式設計”的技術。
例如,我們建立一個ArrayList
的例項,如下(繫結程式碼到抽象):
List<String> products = new ArrayList<>();
我們避免這樣的事情(繫結程式碼到實現):
ArrayList<String> products = new ArrayList<>();
所以,通過第一個例子建立一個ArrayList
例項更好,但是我們也需要宣告一個List
型別的變數。因為List
是一個介面,我們可以很容易的切換到List
的其他實現類,而無需額外的修改。
這就是“面向介面程式設計”,但是var
不能這麼用。這意味著當我們使用var
時,推斷出的型別是實現類的型別。例如,下面這行程式碼,推測出的型別是ArrayList<String>
:
var productList = new ArrayList<String>(); // 推斷為ArrayList<String>
以下幾個論點支援這一行為:
- 首先,
var
是區域性變數,大多數情況下,“面向介面程式設計”在方法引數和返回型別的時候更有用。 - 區域性變數的作用域比較小,切換實現引起的發現和修復成本比較低。
- var將其右側的程式碼視為用於對端實際型別的初始化程式,如果將來修改初始化程式,則推斷型別會改變,從而導致後續依賴此變數的程式碼產生問題。
8. 意外推斷型別的可能性
如果不存在推斷型別所需的資訊,則與菱形運算子組合的var
型別可能導致意外推斷型別。
在Java 7之前的Coin專案中,我們寫了這樣的程式碼:
//顯式指定泛型類的例項化引數型別
List<String> products = new ArrayList<String>();
從Java 7開始,我們有了菱形運算子,它能夠推斷泛型類例項化引數型別:
// inferring generic class's instantiation parameter type
List<String> products = new ArrayList<>();
那麼,以下程式碼推斷出什麼型別呢?
首先應該避免這麼用:
// AVOID
var productList = new ArrayList<>(); // 推斷為ArrayList<Object>
推斷出的型別是Object的ArrayList。之所以會這樣是因為沒有找到能夠推測到預期型別為String的資訊,這會導致返回一個最廣泛可用型別,Object。
所以為了避免這樣的情形,我們必須提供能夠推斷到預測型別的資訊。這個可以直接給也可以間接給。
更好的實現(直接):
// PREFER
var productList = new ArrayList<String>(); // 推斷為ArrayList<String>
更好的實現(間接):
var productStack = new ArrayDeque<String>();
var productList = new ArrayList<>(productStack); // 推斷為ArrayList<String>
更好的實現(間接):
Product p1 = new Product();
Product p2 = new Product();
var listOfProduct = List.of(p1, p2); // 推斷為List<Product>
// 不要這麼幹
var listofProduct = List.of(); // 推斷為List<Object>
listofProduct.add(p1);
listofProduct.add(p2);
9. 賦值陣列到var不需要中括號[]
我們都知道Java中如何宣告一個數組:
int[] numbers = new int[5];
// 或者,這樣寫不太好
int numbers[] = new int[5];
那麼怎麼用var呢?左邊不需要使用括號。
避免這麼寫(編譯不通過):
// 編譯通不過
var[] numbers = new int[5];
// 或者
var numbers[] = new int[5];
應該這麼用:
// PREFER
var numbers = new int[5]; // 推斷為int陣列
numbers[0] = 2; // 對
numbers[0] = 2.2; // 錯
numbers[0] = "2"; // 錯
另外,這麼用也不能編譯,這是因為右邊沒有自己的型別。
// 顯式型別表現符合預期
int[] numbers = {1, 2, 3};
// 編譯通不過
var numbers = {1, 2, 3};
var numbers[] = {1, 2, 3};
var[] numbers = {1, 2, 3};
10. var型別不能被用於複合宣告(一行宣告多個變數)
如果你是複合宣告的粉絲,你一定要知道var
不支援這種宣告。下面的程式碼不能編譯:
// 編譯通不過
// error: 'var' 不允許複合宣告
var hello = "hello", bye = "bye", welcome = "welcome";
用下面的程式碼代替:
// PREFER
String hello = "hello", bye = "bye", welcome = "welcome";
或者這麼用:
// PREFER
var hello = "hello";
var bye = "bye";
var welcome = "welcome";
11. 區域性變數應力求最小化其範圍。var型別強化了這一論點。
區域性變數應該保持小作用域,我確定你在var
出現之前就聽過這個,這樣可以增強程式碼可讀性,也方便更快的修復bug。
例如我們定義一個java棧:
避免:
// AVOID
...
var stack = new Stack<String>();
stack.push("George");
stack.push("Tyllen");
stack.push("Martin");
stack.push("Kelly");
...
// 50行不用stack的程式碼
// George, Tyllen, Martin, Kelly
stack.forEach(...);
...
注意我們呼叫forEach
方法,該方法繼承自java.util.Vector
.這個方法將以Vector的方式遍歷棧。現在我們準備切換Stack
到ArrayDeque
,切換之後forEach()
方法將變成ArrayDeque
的,將以stack(LIFO)的方式遍歷stack。
// AVOID
...
var stack = new ArrayDeque<String>();
stack.push("George");
stack.push("Tyllen");
stack.push("Martin");
stack.push("Kelly");
...
// 50行不用stack的程式碼
// Kelly, Martin, Tyllen, George
stack.forEach(...);
...
這不是我們想要的,我們很難看出引入了一個錯誤,因為包含forEach()
部分的程式碼不在研發完成修改的程式碼附近。為了快速修復這個錯誤,並避免上下滾動來了解發生了什麼,最好縮小stack變數的作用域範圍。
最好這麼寫:
// PREFER
...
var stack = new Stack<String>();
stack.push("George");
stack.push("Tyllen");
stack.push("Martin");
stack.push("Kelly");
...
// George, Tyllen, Martin, Kelly
stack.forEach(...);
...
// 50行不用stack的程式碼
現在,當開發人員從Stack
切換到ArrayQueue
的時候,他們能夠很快的注意到bug,並修復它。
12. var型別便於三元運算子右側的不同型別的運算元
我們可以在三元運算子的右側使用不同型別的運算元。
使用具體型別的時候,以下程式碼無法編譯:
// 編譯通不過
List code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
// or
Set code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
雖然我們可以這麼寫:
Collection code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
Object code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
這樣也編譯不過:
// 編譯通不過:
int code = intOrString ? 12112 : "12112";
String code = intOrString ? 12112 : "12112";
但是我們可以這麼寫:
Serializable code = intOrString ? 12112 : "12112";
Object code = intOrString ? 12112 : "12112";
在這種情況下,使用var
更好:
// PREFER
// inferred as Collection<Integer>
var code = containsDuplicates ? List.of(12, 1, 12) : Set.of(12, 1, 10);
// inferred as Serializable
var code = intOrString ? 12112 : "12112";
千萬不要從這些例子中得出var型別是在執行時做型別推斷的,它不是!!!
當然,我們使用相同的型別作為運算元時var
是支援的。
// 推斷為float
var code = oneOrTwoDigits ? 1211.2f : 1211.25f;
13. var型別能夠用在迴圈體中
我們能非常簡單的在for迴圈中用var
型別取代具體型別。這是兩個例子。
var替換int:
// 顯式型別
for (int i = 0; i < 5; i++) {
...
}
// 使用 var
for (var i = 0; i < 5; i++) { // i 推斷為 int型別
...
}
var替換Order:
List<Order> orderList = ...;
// 顯式型別
for (Order order : orderList) {
...
}
// 使用 var
for (var order : orderList) { // order 推斷成Order型別
...
}
14. var型別能夠和Java 8中的Stream一起用
將Java10中的var與Java 8中的Stream結合起來非常簡單。
你需要使用var取代顯式型別Stream:
例1:
// 顯式型別
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
numbers.filter(t -> t % 2 == 0).forEach(System.out::println);
// 使用 var
var numbers = Stream.of(1, 2, 3, 4, 5); // 推斷為 Stream<Integer>
numbers.filter(t -> t % 2 == 0).forEach(System.out::println);
例2:
// 顯式型別
Stream<String> paths = Files.lines(Path.of("..."));
List<File> files = paths.map(p -> new File(p)).collect(toList());
// 使用 var
var paths = Files.lines(Path.of("...")); // 推斷為 Stream<String>
var files = paths.map(p -> new File(p)).collect(toList()); // 推斷為 List<File>
15. var型別可用於宣告區域性變數,可用於分解表示式巢狀/長鏈
var
型別可用於宣告區域性變數,可用於分解表示式巢狀/長鏈.
大的或者巢狀的表達看起來令人印象深刻,通常它們被認為是聰明的程式碼。有時候我們會故意這麼寫,有時候我們從一個小表示式開始寫,慢慢越來越大。為了提高程式碼可讀性,建議用區域性變數來破壞大型/巢狀表示式。但有時候,新增這些區域性變數是我們想要避免的體力活。如下:
避免:
List<Integer> intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5);
// AVOID
int result = intList.stream()
.collect(Collectors.partitioningBy(i -> i % 2 == 0))
.values()
.stream()
.max(Comparator.comparing(List::size))
.orElse(Collections.emptyList())
.stream()
.mapToInt(Integer::intValue)
.sum();
更好:
List<Integer> intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5);
// PREFER
Map<Boolean, List<Integer>> evenAndOdd = intList.stream()
.collect(Collectors.partitioningBy(i -> i % 2 == 0));
Optional<List<Integer>> evenOrOdd = evenAndOdd.values()
.stream()
.max(Comparator.comparing(List::size));
int sumEvenOrOdd = evenOrOdd.orElse(Collections.emptyList())
.stream()
.mapToInt(Integer::intValue)
.sum();
第二段程式碼可讀性更強,更簡潔,但是第一段程式碼也是對的。我們的思維會適應這樣的大表達式並且更喜歡它們而不是區域性變數。然而,使用var型別對於使用區域性變數的方式來說是一個優化,因為它節省了獲取顯式型別的時間。
更好
var intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5);
// PREFER
var evenAndOdd = intList.stream()
.collect(Collectors.partitioningBy(i -> i % 2 == 0));
var evenOrOdd = evenAndOdd.values()
.stream()
.max(Comparator.comparing(List::size));
var sumEvenOrOdd = evenOrOdd.orElse(Collections.emptyList())
.stream()
.mapToInt(Integer::intValue)
.sum();
16. var型別不能被用於方法返回型別或者方法引數型別。
試著寫下面的兩段程式碼,編譯通不過。
使用var作為方法返回型別:
// 編譯通不過
public var countItems(Order order, long timestamp) {
...
}
使用var作為方法引數型別:
// 編譯通不過
public int countItems(var order, var timestamp) {
...
}
17. var型別的區域性變數可以用來傳入到方法引數,也可以用來存放方法返回值
var
型別的區域性變數可以用來傳入到方法引數,也可以用來存放方法返回值。下面這兩段程式碼能夠編譯而且執行。
public int countItems(Order order, long timestamp) {
...
}
public boolean checkOrder() {
var order = ...; // Order例項
var timestamp = ...; // long型別的 timestamp
var itemsNr = countItems(order, timestamp); // 推斷為int型別
...
}
它也適用於泛型。下面的程式碼片段也是對的。
public <A, B> B contains(A container, B tocontain) {
...
}
var order = ...; // Order例項
var product = ...; // Product例項
var resultProduct = contains(order, product); // inferred as Product type
18. var型別能和匿名類一起使用。
避免:
public interface Weighter {
int getWeight(Product product);
}
// AVOID
Weighter weighter = new Weighter() {
@Override
public int getWeight(Product product) {
...
}
};
Product product = ...; // Product例項
int weight = weighter.getWeight(product);
更好的程式碼:
public interface Weighter {
int getWeight(Product product);
}
// PREFER
var weighter = new Weighter() {
@Override
public int getWeight(Product product) {
...
}
};
var product = ...; // Product例項
var weight = weighter.getWeight(product);
19. var型別可以是Effectively Final
從Java SE 8開始,區域性類可以訪問封閉塊內final或者effectively final的引數。變數初始化後不再改變的引數為effectively final。
所以,var型別的變數可以是effectively final的。我們可以從以下程式碼中看到。
避免:
public interface Weighter {
int getWeight(Product product);
}
// AVOID
int ratio = 5; // 這是effectively final
Weighter weighter = new Weighter() {
@Override
public int getWeight(Product product) {
return ratio * ...;
}
};
ratio = 3; // 這行賦值語句會報錯
更好:
public interface Weighter {
int getWeight(Product product);
}
// PREFER
var ratio = 5; // 這是effectively final
var weighter = new Weighter() {
@Override
public int getWeight(Product product) {
return ratio * ...;
}
};
ratio = 3; // 這行賦值語句會報錯
20. var型別可以用final修飾
預設情況下,var型別的區域性變數可以被重新賦值(除非它是effectively final的)。但是我們可以宣告它為final型別,如下:
避免:
// AVOID
// IT DOESN'T COMPILE
public void discount(int price) {
final int limit = 2000;
final int discount = 5;
if (price > limit) {
discount++; // 這行會報錯
}
}
更好:
// PREFER
// IT DOESN'T COMPILE
public void discount(int price) {
final var limit = 2000;
final var discount = 5;
if (price > limit) {
discount++; // 這行會報錯
}
}
21. Lambda表示式和方法引用需要顯示物件型別
當對應的型別推斷不出來時不能使用var型別。所以,lambda表示式和方法引用初始化不被允許。這是var型別限制的一部分。
下面的程式碼無法編譯:
// 編譯不通過
// lambda表示式需要顯式目標型別
var f = x -> x + 1;
// 方法引用需要顯式目標型別
var exception = IllegalArgumentException::new;
用以下程式碼代替:
// PREFER
Function<Integer, Integer> f = x -> x + 1;
Supplier<IllegalArgumentException> exception = IllegalArgumentException::new;
但是在lambda的內容中,Java 11允許我們去使用var作為lambda引數。例如,以下程式碼在Java 11中可以很好的工作(詳見JEP 323(lambda引數中的區域性變數))
// Java 11
(var x, var y) -> x + y
// or
(@Nonnull var x, @Nonnull var y) -> x + y
22. 為var型別賦值為null是不被允許的。
此外,也不允許缺少初始化程式。這是var型別的另一個限制。
以下程式碼不會編譯通過(賦值null):
// 編譯通不過
var message = null; // 型別錯誤: 變數初始化為'null'
這個程式碼也不會編譯通過(缺少初始化):
// IT DOESN'T COMPILE
var message; // 使用var不能不做初始化
...
message = "hello";
更好:
// PREFER
String message = null;
// or
String message;
...
message = "hello";
23. var型別不能作為物件的域(Field)
var型別可以用來做區域性變數,但是不能用來做物件的域/全域性變數。
這個限制會導致這裡的編譯錯誤:
// 編譯通不過
public class Product {
private var price; // 'var' 不被允許
private var name; // 'var' 不被允許
...
}
用以下程式碼代替:
// PREFER
public class Product {
private int price;
private String name;
...
}
24. var不被允許在catch塊中使用
但是它被允許在try-with-resources中。
catch塊
當代碼丟擲異常時,我們必須通過顯式型別catch它,因為var型別不被允許。這個限制會導致以下程式碼的編譯時錯誤:
// 編譯通不過
try {
TimeUnit.NANOSECONDS.sleep(5000);
} catch (var ex) {
...
}
用這個取代:
// PREFER
try {
TimeUnit.NANOSECONDS.sleep(5000);
} catch (InterruptedException ex) {
...
}
try-with-resources
另一方面,var型別可以用在try-with-resource中,例如:
// 顯式型別
try (PrintWriter writer = new PrintWriter(new File("welcome.txt"))) {
writer.println("Welcome message");
}
可以用var重寫:
// 使用 var
try (var writer = new PrintWriter(new File("welcome.txt"))) {
writer.println("Welcome message");
}
25. var型別不能和泛型T一起使用
假定我們有下面的程式碼:
public <T extends Number> T add(T t) {
T temp = t;
...
return temp;
}
這種情況下,使用var的執行結果是符合預期的,我們可以用var替換T,如下:
public <T extends Number> T add(T t) {
var temp = t;
...
return temp;
}
我們看一下另一個var能夠成功使用的例子,如下:
public <T extends Number> T add(T t) {
List<T> numbers = new ArrayList<>();
numbers.add((T) Integer.valueOf(3));
numbers.add((T) Double.valueOf(3.9));
numbers.add(t);
numbers.add("5"); // 錯誤:型別不相容,string不能轉為T
...
}
可以用var取代List
public <T extends Number> T add(T t) {
var numbers = new ArrayList<T>();
// DON'T DO THIS, DON'T FORGET THE, T
var numbers = new ArrayList<>();
numbers.add((T) Integer.valueOf(3));
numbers.add((T) Double.valueOf(3.9));
numbers.add(t);
numbers.add("5"); // // 錯誤:型別不相容,string不能轉為T
...
}
26. 使用帶有var型別的萬用字元(?),協方差和反對變數時要特別注意
使用?萬用字元
這麼做是安全的:
// 顯式型別
Class<?> clazz = Integer.class;
// 使用var
var clazz = Integer.class;
但是,不要因為程式碼中有錯誤,而var可以讓它們魔法般的消失,就使用var取代Foo<?>。看下一個例子,不是非常明顯,但是我想讓它指出核心。考慮一下當你編寫這一段程式碼的過程,也許,你嘗試定義一個String的ArrayList,並最終定義成了Collection<?>。
// 顯式型別
Collection<?> stuff = new ArrayList<>();
stuff.add("hello"); // 編譯錯誤
stuff.add("world"); // 編譯錯誤
// 使用var,錯誤會消失,但是我不確定你是你想要的結果
var stuff = new ArrayList<>();
strings.add("hello"); // 錯誤消失
strings.add("world"); // 錯誤消失
Java協變(Foo<? extends T>)和逆變(Foo<? super T>)
我們知道可以這麼寫:
// 顯式型別
Class<? extends Number> intNumber = Integer.class;
Class<? super FilterReader> fileReader = Reader.class;
而且如果我們錯誤賦值了錯誤的型別,接收到一個編譯時錯誤,這就是我們想要的:
// 編譯通不過
// 錯誤: Class<Reader> 不能轉換到 Class<? extends Number>
Class<? extends Number> intNumber = Reader.class;
// 錯誤: Class<Integer> 不能轉化到Class<? super FilterReader>
Class<? super FilterReader> fileReader = Integer.class;
但是如果我們使用var:
// using var
var intNumber = Integer.class;
var fileReader = Reader.class;
然後我們可以為這些變數賦值任何類,因此我們的邊界/約束消失了,這並不是我們想要的。
// 編譯通過
var intNumber = Reader.class;
var fileReader = Integer.class;