第三篇 : 局部變量類型推斷

一、簡(jiǎn)介

Java局部變量類型推斷(LVTI),簡(jiǎn)稱var類型(標(biāo)識(shí)符var不是一個(gè)關(guān)鍵字,是一個(gè)預(yù)留類型名),Java10中新添加的功能。作為100%編譯特征,它不會(huì)影響字節(jié)碼,運(yùn)行時(shí)或者性能。在編譯時(shí),編譯器會(huì)檢查賦值語(yǔ)句右側(cè)代碼,從而推斷出具體類型。它查看聲明的右側(cè),如果這是一個(gè)初始化語(yǔ)句,它會(huì)用那個(gè)類型取代var。另外,它非常有助于減少冗余代碼和樣板代碼。

二、使用及注意事項(xiàng)

1. 爭(zhēng)取起有意義的局部變量名

通常我們?cè)谄鹑肿兞棵臅r(shí)候會(huì)注意這一點(diǎn),但是選擇局部變量名的時(shí)候不太注意。尤其是當(dāng)方法很短,方法名和實(shí)現(xiàn)都不錯(cuò)的時(shí)候,我們趨向于簡(jiǎn)化我們的變量名。但是當(dāng)我們使用var替代顯式類型的時(shí)候,具體的類型是通過(guò)編譯器推斷出來(lái)的。所以,對(duì)于人來(lái)說(shuō)閱讀或者理解代碼非常困難。在這一點(diǎn)上var削弱了代碼可讀性。這種事情之所以會(huì)發(fā)生,是因?yàn)榇蠖鄶?shù)情況下,我們會(huì)把變量類型當(dāng)成是第一信息,而把變量名當(dāng)成第二信息。但是使用var的時(shí)候,恰恰相反。

示例1

即使到這里,一些朋友仍然堅(jiān)持局部變量名短點(diǎn)好。我們看一下:

// HAVING
public boolean callDocumentationTask() {
    DocumentationTool dtl = ToolProvider.getSystemDocumentationTool();
    DocumentationTask dtt = dtl.getTask(...);
    return dtt.call();
}

我們換成var時(shí),避免:

public boolean callDocumentationTask() {
    var dtl = ToolProvider.getSystemDocumentationTool();
    var dtt = dtl.getTask(...);
    return dtt.call();
}

更好:

public boolean callDocumentationTask() {
    var documentationTool = ToolProvider.getSystemDocumentationTool();
    var documentationTask = documentationTool.getTask(...);
  return documentationTask.call();
}

示例2

避免:

public List<Product> fetchProducts(long userId) {
    var u = userRepository.findById(userId);
    var p = u.getCart();
    return p;
}

更好:

public List<Product> fetchProducts(long userId) {
    var user = userRepository.findById(userId);
    var productList = user.getCart();
    return productList;
}

示例3

爭(zhēng)取為局部變量起有意義的名字并不意味著要掉入過(guò)度命名的坑,避免在短方法中使用單一類型的數(shù)據(jù)流:

var byteArrayOutputStream = new ByteArrayOutputStream();

用如下代碼代替更加清晰:

var outputStream = new ByteArrayOutputStream();
// or
var outputStreamOfFoo = new ByteArrayOutputStream();

2. 使用數(shù)據(jù)類型標(biāo)志來(lái)幫助var去推斷出預(yù)期的基本數(shù)據(jù)類型(int, long, float, double)

如果在基本數(shù)據(jù)類型中不使用有效的數(shù)據(jù)類型標(biāo)志,我們可能會(huì)發(fā)現(xiàn)預(yù)期的類型和推測(cè)出的類型不一致。這是由于var的隱式類型轉(zhuǎn)換導(dǎo)致的。

例如,下面兩行代碼的表現(xiàn)是符合預(yù)期的,首先,我們聲明一個(gè)boolean 和一個(gè)char使用顯式類型:

boolean flag = true; // 這是一個(gè)boolean類型
char a = 'a';        // 這是一個(gè)char類型

現(xiàn)在,我們使用var代替顯式基本類型:

var flag = true; // 被推斷為boolean類型
var a = 'a';     // 被推斷為char類型

到目前為止,一切都很完美。接下來(lái),我們看一下相同邏輯下的int, long, double 和 float:

int intNumber = 20;       // 這是int類型
long longNumber = 20;     // 這是long類型
float floatNumber = 20;   // 這是float類型, 20.0
double doubleNumber = 20; // 這是double類型, 20.0

以上代碼是很常見(jiàn)而且清晰的,現(xiàn)在我們使用var:

避免:

var intNumber = 20;    // 推斷為int
var longNumber = 20;   // 推斷為int
var floatNumber = 20;  // 推斷為int
var doubleNumber = 20; // 推斷為int

更好:

var intNumber = 20;     // 推斷為int
var longNumber = 20L;   // 推斷為long
var floatNumber = 20F;  // 推斷為float, 20.0
var doubleNumber = 20D; // 推斷為double, 20.0

但是如果我們使用小數(shù)聲明一個(gè)數(shù)字,會(huì)發(fā)生什么呢?當(dāng)你認(rèn)為你的數(shù)字是一個(gè)float的時(shí)候,避免這樣做:

// 避免,如果這是一個(gè)float
var floatNumber = 20.5; // 推斷為double

你應(yīng)該用對(duì)應(yīng)的數(shù)據(jù)類型標(biāo)志來(lái)避免這樣的問(wèn)題:

// 更好, 如果這是一個(gè)float
var floatNumber = 20.5F; // 推斷為float

3. 在某些情況下,Var和隱式類型轉(zhuǎn)換可以維持可維護(hù)性

在某些情況下,Var和隱式類型轉(zhuǎn)換可以維持可維護(hù)性。例如,假設(shè)我們的代碼包含兩個(gè)方法:第一個(gè)方法接收一個(gè)包含不同條目的購(gòu)物卡,比較市場(chǎng)中不同的價(jià)格,計(jì)算出最好的價(jià)格,并匯總返回float類型的總價(jià)。另一個(gè)方法簡(jiǎn)單的把這個(gè)float價(jià)格從卡中扣除。

首先,我們看一下計(jì)算最好價(jià)格的方法:

public float computeBestPrice(String[] items) {
   ...
   float price = ...;
   return price;
}

然后,我們看一下扣款的方法:

public boolean debitCard(float amount, ...) {
    ...
}

現(xiàn)在,我們把這兩個(gè)方法匯總,提供一個(gè)服務(wù)方法。顧客選擇要買(mǎi)的商品,計(jì)算最優(yōu)價(jià)格,然后扣款:

//避免
public void purchaseCart(long customerId) {
    ...
    float price = computeBestPrice(...);
    debitCard(price, ...);
}

一段時(shí)間后,公司想要去除價(jià)格中的小數(shù)部分作為打折策略,使用int代替了float, 我們需要修改代碼:

public int computeBestPrice(String[] items) {
   ...
   float realprice = ...;
   ...
   int price = (int) realprice;
   return price;
}
public boolean debitCard(int amount, ...) {
    ...
}

問(wèn)題在于我們使用了顯示類型float,這樣的更改不能被兼容。代碼會(huì)報(bào)編譯時(shí)錯(cuò)誤。但是如果我們預(yù)判到這種情況,使用var代替float, 我們的代碼會(huì)因?yàn)殡[式類型轉(zhuǎn)換而變得沒(méi)有兼容性問(wèn)題。

//更好
public void purchaseCart(long customerId) {
    ...
    var price = computeBestPrice(...);
    debitCard(price, ...);
}

4. 當(dāng)數(shù)據(jù)類型標(biāo)志解決不了問(wèn)題的時(shí)候,依賴顯式向下轉(zhuǎn)換或者避免var

一些Java基礎(chǔ)數(shù)據(jù)類型不支持?jǐn)?shù)據(jù)類型標(biāo)志。例如byte和short。使用顯式基礎(chǔ)數(shù)據(jù)類型時(shí)沒(méi)有任何問(wèn)題。使用var代替的時(shí)候:

// 這樣更好,而不是使用var
byte byteNumber = 45;     // 這是byte類型
short shortNumber = 4533; // 這是short類型

為什么在這種情況下顯式類型比var好呢?我們切換到var.注意示例中都會(huì)被推斷為int, 而不是我們預(yù)期的類型。

避免使用以下代碼:

var byteNumber = 45;    // 推斷為int
var shortNumber = 4533; // 推斷為int

這里沒(méi)有基礎(chǔ)數(shù)據(jù)類型幫助我們,因此我們需要依賴顯示強(qiáng)制類型轉(zhuǎn)換。從個(gè)人角度來(lái)講,我會(huì)避免這么用,因?yàn)闆](méi)啥好處,但是可以這么用。

如果你真的想用var,這么用:

var byteNumber = (byte) 45;     // 推斷為byte
var shortNumber = (short) 4533; // 推斷為short

5. 如果變量名沒(méi)有對(duì)人來(lái)說(shuō)足夠的類型信息,避免使用var

使用var有助于提供更加簡(jiǎn)練的代碼。例如, 在使用構(gòu)造方法時(shí)(這是使用局部變量的常見(jiàn)示例),我們可以簡(jiǎn)單地避免重復(fù)類名的必要性,從而消除冗余。

避免:

MemoryCacheImageInputStream inputStream = new MemoryCacheImageInputStream(...);

更好:

var inputStream = new MemoryCacheImageInputStream(...);

在下面的結(jié)構(gòu)中,var也是一個(gè)簡(jiǎn)化代碼而不丟失信息的好方法。

避免:

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager fm = compiler.getStandardFileManager(...);

更好:

var compiler = ToolProvider.getSystemJavaCompiler();
var fileManager = compiler.getStandardFileManager(...);

為什么這樣基于var的例子我們感覺(jué)比較舒服呢?因?yàn)樾枰男畔⒁呀?jīng)在變量名中了。但是如果使用var 加上變量名,還是會(huì)丟失信息,那么最好避免使用var。

避免:

public File fetchCartContent() {
    return new File(...);
}
// As a human, is hard to infer the "cart" type without 
// inspecting the fetchCartContent() method
var cart = fetchCartContent();

使用以下代碼代替:

public File fetchCartContent() {
    return new File(...);
}
File cart = fetchCartContent();

思考一個(gè)基于java.nio.channels.Selector的例子。這個(gè)類有一個(gè)靜態(tài)方法叫做open(),返回一個(gè)新的Selector實(shí)例并且執(zhí)行open動(dòng)作。但是Selector.open()很容易被認(rèn)為返回一個(gè)boolean標(biāo)識(shí)打開(kāi)當(dāng)前選擇器是否成功,或者返回void。使用var導(dǎo)致丟失信息會(huì)引發(fā)這樣的困擾。

6. var類型確保編譯時(shí)安全

var類型是編譯時(shí)安全的。這意味著如果我們?cè)噲D實(shí)現(xiàn)一個(gè)錯(cuò)的賦值,會(huì)導(dǎo)致編譯時(shí)報(bào)錯(cuò)。例如,以下代碼編譯不會(huì)通過(guò)。

// 編譯通不過(guò)
var items = 10;
items = "10 items"; // 不兼容類型: String不能轉(zhuǎn)為int

以下代碼編譯會(huì)通過(guò):

var items = 10;
items = 20;

這個(gè)代碼也會(huì)編譯通過(guò):

var items = "10";
items = "10 items" ;

所以,一旦編譯器已經(jīng)推斷出了var對(duì)應(yīng)的類型,我們只能賦值對(duì)應(yīng)類型的值給它。

7. var 不能被用于將真實(shí)類型的實(shí)例賦值給接口類型變量

在Java中,我們使用“面向接口編程”的技術(shù)。

例如,我們創(chuàng)建一個(gè)ArrayList的實(shí)例,如下(綁定代碼到抽象):

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

我們避免這樣的事情(綁定代碼到實(shí)現(xiàn)):

ArrayList<String> products = new ArrayList<>();

所以,通過(guò)第一個(gè)例子創(chuàng)建一個(gè)ArrayList實(shí)例更好,但是我們也需要聲明一個(gè)List類型的變量。因?yàn)長(zhǎng)ist是一個(gè)接口,我們可以很容易的切換到List的其他實(shí)現(xiàn)類,而無(wú)需額外的修改。

這就是“面向接口編程”,但是var不能這么用。這意味著當(dāng)我們使用var時(shí),推斷出的類型是實(shí)現(xiàn)類的類型。例如,下面這行代碼,推測(cè)出的類型是ArrayList<String>:

var productList = new ArrayList<String>(); // 推斷為ArrayList<String>

以下幾個(gè)論點(diǎn)支持這一行為:

  • 首先,var是局部變量,大多數(shù)情況下,“面向接口編程”在方法參數(shù)和返回類型的時(shí)候更有用。
  • 局部變量的作用域比較小,切換實(shí)現(xiàn)引起的發(fā)現(xiàn)和修復(fù)成本比較低。
  • var將其右側(cè)的代碼視為用于對(duì)端實(shí)際類型的初始化程序,如果將來(lái)修改初始化程序,則推斷類型會(huì)改變,從而導(dǎo)致后續(xù)依賴此變量的代碼產(chǎn)生問(wèn)題。

8. 意外推斷類型的可能性

如果不存在推斷類型所需的信息,則與菱形運(yùn)算符組合的var類型可能導(dǎo)致意外推斷類型。

在Java 7之前的Coin項(xiàng)目中,我們寫(xiě)了這樣的代碼:

//顯式指定泛型類的實(shí)例化參數(shù)類型
List<String> products = new ArrayList<String>();

從Java 7開(kāi)始,我們有了菱形運(yùn)算符,它能夠推斷泛型類實(shí)例化參數(shù)類型:

// inferring generic class's instantiation parameter type 
List<String> products = new ArrayList<>();

那么,以下代碼推斷出什么類型呢?

首先應(yīng)該避免這么用:

var productList = new ArrayList<>(); // 推斷為ArrayList<Object>

推斷出的類型是Object的ArrayList。之所以會(huì)這樣是因?yàn)闆](méi)有找到能夠推測(cè)到預(yù)期類型為String的信息,這會(huì)導(dǎo)致返回一個(gè)最廣泛可用類型,Object。

所以為了避免這樣的情形,我們必須提供能夠推斷到預(yù)測(cè)類型的信息。這個(gè)可以直接給也可以間接給。

更好的實(shí)現(xiàn)(直接):

var productList = new ArrayList<String>(); // 推斷為ArrayList<String>

更好的實(shí)現(xiàn)(間接):

var productStack = new ArrayDeque<String>(); 
var productList = new ArrayList<>(productStack); // 推斷為ArrayList<String>

更好的實(shí)現(xiàn)(間接):

Product p1 = new Product();
Product p2 = new Product();
var listOfProduct = List.of(p1, p2); // 推斷為L(zhǎng)ist<Product>
// 不要這么干
var listofProduct = List.of(); // 推斷為L(zhǎng)ist<Object>
listofProduct.add(p1); //報(bào)錯(cuò)
listofProduct.add(p2); //報(bào)錯(cuò)

9. 賦值數(shù)組到var不需要中括號(hào)[]

我們都知道Java中如何聲明一個(gè)數(shù)組:

int[] numbers = new int[5];
// 或者,這樣寫(xiě)不太好
int numbers[] = new int[5];

那么怎么用var呢?左邊不需要使用括號(hào)。

避免這么寫(xiě)(編譯不通過(guò)):

// 編譯通不過(guò)
var[] numbers = new int[5];
// 或者
var numbers[] = new int[5];

應(yīng)該這么用:

var numbers = new int[5]; // 推斷為int數(shù)組
numbers[0] = 2;   // 對(duì)
numbers[0] = 2.2; // 錯(cuò)
numbers[0] = "2"; // 錯(cuò)

另外,這么用也不能編譯,這是因?yàn)橛疫厸](méi)有自己的類型:

// 顯式類型表現(xiàn)符合預(yù)期
int[] numbers = {1, 2, 3};
// 編譯通不過(guò)
var numbers = {1, 2, 3};
var numbers[] = {1, 2, 3};
var[] numbers = {1, 2, 3};

10. var類型不能被用于復(fù)合聲明(一行聲明多個(gè)變量)

如果你喜歡復(fù)合聲明,你一定要知道var不支持這種聲明。下面的代碼不能編譯:

// 編譯通不過(guò)
// error: 'var' 不允許復(fù)合聲明
var hello = "hello", bye = "bye", welcome = "welcome";

用下面的代碼代替:

String hello = "hello", bye = "bye", welcome = "welcome";

或者這么用:

var hello = "hello";
var bye = "bye";
var welcome = "welcome";

11. 局部變量應(yīng)力求最小化其范圍。var類型強(qiáng)化了這一論點(diǎn)

局部變量應(yīng)該保持小作用域,我確定你在var出現(xiàn)之前就聽(tīng)過(guò)這個(gè),這樣可以增強(qiáng)代碼可讀性,也方便更快的修復(fù)bug。

例如我們定義一個(gè)java棧:

避免:

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

這不是我們想要的,我們很難看出引入了一個(gè)錯(cuò)誤,因?yàn)榘琭orEach()部分的代碼不在研發(fā)完成修改的代碼附近。為了快速修復(fù)這個(gè)錯(cuò)誤,并避免上下滾動(dòng)來(lái)了解發(fā)生了什么,最好縮小stack變量的作用域范圍。

最好這么寫(xiě):

...
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的代碼

現(xiàn)在,當(dāng)開(kāi)發(fā)人員從Stack切換到ArrayQueue的時(shí)候,他們能夠很快的注意到bug,并修復(fù)它。

12. var類型便于三元運(yùn)算符右側(cè)的不同類型的操作數(shù)

我們可以在三元運(yùn)算符的右側(cè)使用不同類型的操作數(shù)。

使用具體類型的時(shí)候,以下代碼無(wú)法編譯:

// 編譯通不過(guò)
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);

雖然我們可以這么寫(xiě):

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

這樣也編譯不過(guò):

// 編譯通不過(guò):
int code = intOrString ? 12112 : "12112";
String code = intOrString ? 12112 : "12112";

但是我們可以這么寫(xiě):

Serializable code = intOrString ? 12112 : "12112";
Object code = intOrString ? 12112 : "12112";

在這種情況下,使用var更好:

// 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";

千萬(wàn)不要從這些例子中得出var類型是在運(yùn)行時(shí)做類型推斷的,它不是!??!

當(dāng)然,我們使用相同的類型作為操作數(shù)時(shí)var是支持的。

// 推斷為float
var code = oneOrTwoDigits ? 1211.2f : 1211.25f;

13. var類型能夠用在循環(huán)體中

我們能非常簡(jiǎn)單的在for循環(huán)中用var類型取代具體類型。這是兩個(gè)例子。

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結(jié)合起來(lái)非常簡(jiǎn)單。

你需要使用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類型可用于聲明局部變量,可用于分解表達(dá)式嵌套/長(zhǎng)鏈

大的或者嵌套的表達(dá)看起來(lái)令人印象深刻,通常它們被認(rèn)為是聰明的代碼。有時(shí)候我們會(huì)故意這么寫(xiě),有時(shí)候我們從一個(gè)小表達(dá)式開(kāi)始寫(xiě),慢慢越來(lái)越大。為了提高代碼可讀性,建議用局部變量來(lái)破壞大型/嵌套表達(dá)式。但有時(shí)候,添加這些局部變量是我們想要避免的體力活。如下:

避免:

List<Integer> intList = List.of(1, 1, 2, 3, 4, 4, 6, 2, 1, 5, 4, 5);
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();

第二段代碼可讀性更強(qiáng),更簡(jiǎn)潔,但是第一段代碼也是對(duì)的。我們的思維會(huì)適應(yīng)這樣的大表達(dá)式并且更喜歡它們而不是局部變量。然而,使用var類型對(duì)于使用局部變量的方式來(lái)說(shuō)是一個(gè)優(yōu)化,因?yàn)樗?jié)省了獲取顯式類型的時(shí)間。

更好:

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

16. var類型不能被用于方法返回類型或者方法參數(shù)類型

試著寫(xiě)下面的兩段代碼,編譯通不過(guò)。

使用var作為方法返回類型:

// 編譯通不過(guò)
public var countItems(Order order, long timestamp) {
    ...        
}

使用var作為方法參數(shù)類型:

// 編譯通不過(guò)
public int countItems(var order, var timestamp) {
    ...  
}

17. var類型的局部變量可以用來(lái)傳入到方法參數(shù),也可以用來(lái)存放方法返回值

下面這兩段代碼能夠編譯而且運(yùn)行:

public int countItems(Order order, long timestamp) {
    ...
}
public boolean checkOrder() {
    var order = ...;     // Order實(shí)例
    var timestamp = ...; // long類型的 timestamp
    var itemsNr = countItems(order, timestamp); // 推斷為int類型
    ...
}

它也適用于泛型。下面的代碼片段也是對(duì)的:

public <A, B> B contains(A container, B tocontain) {
    ...
}
var order = ...;   // Order實(shí)例
var product = ...; // Product實(shí)例
var resultProduct = contains(order, product); // inferred as Product type

18. var類型能和匿名類一起使用

避免:

public interface Weighter {
    int getWeight(Product product);
}
Weighter weighter = new Weighter() {
    @Override
    public int getWeight(Product product) {
        ...
    }
};
Product product = ...; // Product實(shí)例
int weight = weighter.getWeight(product);

更好:

public interface Weighter {
    int getWeight(Product product);
}
var weighter = new Weighter() {
    @Override
    public int getWeight(Product product) {
        ...
    }
};
var product = ...; // Product實(shí)例
var weight = weighter.getWeight(product);

19. var類型可以是Effectively Final

從Java SE 8開(kāi)始,局部類可以訪問(wèn)封閉塊內(nèi)final或者effectively final的參數(shù)。變量初始化后不再改變的參數(shù)為effectively final。

所以,var類型的變量可以是effectively final的。我們可以從以下代碼中看到。

避免:

public interface Weighter {
    int getWeight(Product product);
}
int ratio = 5; // 這是effectively final
Weighter weighter = new Weighter() {
    @Override
    public int getWeight(Product product) {
        return ratio * ...;
    }
};
ratio = 3; // 這行賦值語(yǔ)句會(huì)報(bào)錯(cuò)

更好:

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; // 這行賦值語(yǔ)句會(huì)報(bào)錯(cuò)

20. var類型可以用final修飾

默認(rèn)情況下,var類型的局部變量可以被重新賦值(除非它是effectively final的)。但是我們可以聲明它為final類型,如下:

避免:

public void discount(int price) {
    final int limit = 2000;
    final int discount = 5;
    if (price > limit) {
        discount++; // 這行會(huì)報(bào)錯(cuò)
    }
}

更好:

public void discount(int price) {
    final var limit = 2000;
    final var discount = 5;
    if (price > limit) {
        discount++; // 這行會(huì)報(bào)錯(cuò)
    }
}

21. Lambda表達(dá)式和方法引用需要顯示對(duì)象類型

當(dāng)對(duì)應(yīng)的類型推斷不出來(lái)時(shí)不能使用var類型。所以,lambda表達(dá)式和方法引用初始化不被允許。這是var類型限制的一部分。

下面的代碼無(wú)法編譯:

// 編譯不通過(guò)
// lambda表達(dá)式需要顯式目標(biāo)類型
var f = x -> x + 1;
// 方法引用需要顯式目標(biāo)類型
var exception = IllegalArgumentException::new;

用以下代碼代替:

Function<Integer, Integer> f = x -> x + 1;
Supplier<IllegalArgumentException> exception = IllegalArgumentException::new;

但是在lambda的內(nèi)容中,Java 11允許我們?nèi)ナ褂胿ar作為lambda參數(shù)。例如,以下代碼在Java 11中可以很好的工作(詳見(jiàn)JEP 323(lambda參數(shù)中的局部變量))

// Java 11
(var x, var y) -> x + y
// or 
(@Nonnull var x, @Nonnull var y) -> x + y

22. 為var類型賦值為null是不被允許的

此外,也不允許缺少初始化程序。這是var類型的另一個(gè)限制。

以下代碼不會(huì)編譯通過(guò)(賦值null):

// 編譯通不過(guò)
var message = null; // 類型錯(cuò)誤: 變量初始化為'null'

這個(gè)代碼也不會(huì)編譯通過(guò)(缺少初始化):

// IT DOESN'T COMPILE
var message; // 使用var不能不做初始化
...
message = "hello";

更好:

String message = null;
// or
String message;
...
message = "hello";

23. var類型不能作為成員屬性(Field)

var類型可以用來(lái)做局部變量,但是不能用來(lái)做對(duì)象的域/全局變量。

這個(gè)限制會(huì)導(dǎo)致這里的編譯錯(cuò)誤:

// 編譯通不過(guò)
public class Product {
    private var price; // 'var' 不被允許
    private var name;  // 'var' 不被允許
    ...
}

用以下代碼代替:

public class Product {
    private int price; 
    private String name;
    ...
}

24. var不被允許在catch塊中使用

但是它被允許在try-with-resources中。

catch塊

當(dāng)代碼拋出異常時(shí),我們必須通過(guò)顯式類型catch它,因?yàn)関ar類型不被允許。這個(gè)限制會(huì)導(dǎo)致以下代碼的編譯時(shí)錯(cuò)誤:

// 編譯通不過(guò)
try {
    TimeUnit.NANOSECONDS.sleep(5000);
} catch (var ex) {
    ...
}

用這個(gè)取代:

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重寫(xiě):

// 使用 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的運(yùn)行結(jié)果是符合預(yù)期的,我們可以用var替換T,如下:

public <T extends Number> T add(T t) {
     var temp = t;
     ...
     return temp;   
}

我們看一下另一個(gè)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"); // 錯(cuò)誤:類型不兼容,string不能轉(zhuǎn)為T(mén)
     ...     
}

可以用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"); // // 錯(cuò)誤:類型不兼容,string不能轉(zhuǎn)為T(mén)
     ...     
}

這么做是安全的:

// 顯式類型
Class<?> clazz = Integer.class;
// 使用var
var clazz = Integer.class;

但是,不要因?yàn)榇a中有錯(cuò)誤,而var可以讓它們魔法般的消失,就使用var取代Foo<?>??聪乱粋€(gè)例子,不是非常明顯,但是我想讓它指出核心??紤]一下當(dāng)你編寫(xiě)這一段代碼的過(guò)程,也許,你嘗試定義一個(gè)String的ArrayList,并最終定義成了Collection<?>。

26. 使用帶有var類型的通配符(?),協(xié)變和你變時(shí)要特別注意

使用 ?通配符

// 顯式類型
Collection<?> stuff = new ArrayList<>();
stuff.add("hello"); // 編譯錯(cuò)誤
stuff.add("world"); // 編譯錯(cuò)誤
// 使用var,錯(cuò)誤會(huì)消失,但是我不確定你是你想要的結(jié)果
var stuff = new ArrayList<>();
strings.add("hello"); // 錯(cuò)誤消失
strings.add("world"); // 錯(cuò)誤消失

Java 協(xié)變(Foo<? extends T>)和 逆變(Foo<? super T>)

我們知道可以這么寫(xiě):

// 顯式類型
Class<? extends Number> intNumber = Integer.class;
Class<? super FilterReader> fileReader = Reader.class;

而且如果我們錯(cuò)誤賦值了錯(cuò)誤的類型,接收到一個(gè)編譯時(shí)錯(cuò)誤,這就是我們想要的:

// 編譯通不過(guò)
// 錯(cuò)誤: Class<Reader> 不能轉(zhuǎn)換到 Class<? extends Number>
Class<? extends Number> intNumber = Reader.class;
// 錯(cuò)誤: Class<Integer> 不能轉(zhuǎn)化到Class<? super FilterReader>
Class<? super FilterReader> fileReader = Integer.class;

但是如果我們使用var:

// using var
var intNumber = Integer.class;
var fileReader = Reader.class;

然后我們可以為這些變量賦值任何類,因此我們的邊界/約束消失了,這并不是我們想要的:

// 編譯通過(guò)
var intNumber = Reader.class;
var fileReader = Integer.class;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容