Java 10 var關鍵字詳解和示例教程

在本文中,我將通過示例介紹新的Java SE 10特性——“var”類型。你將學習如何在代碼中正確使用它,以及在什麼情況下不能使用它。

介紹

Java 10引入了一個閃亮的新功能:局部變量類型推斷。對於局部變量,現在可以使用特殊的保留類型名稱“var”代替實際類型,如下所示:

var name = “Mohamed Taman”;
提供這個特性是爲了增強Java語言,並將類型推斷擴展到局部變量的聲明上。這樣可以減少板代碼,同時仍然保留Java的編譯時類型檢查。

由於編譯器需要通過檢查賦值等式右側(RHS)來推斷var的實際類型,因此在某些情況下,這個特性具有侷限性。我會在稍後提到這個問題。現在,讓我們來看一些簡單的例子吧。

在開始演示代碼之前,你需要一個IDE來體驗這些新特性。現在有很多可選擇的IDE,所以你可以在它們當中選擇你喜歡的能夠支持Java SE 10的IDE,比如Apache NetBeans 9、IntelliJ IDEA 2018或最新版本的Eclipse。

就個人而言,我更喜歡使用交互式的編程工具,可以快速學習Java語言語法,瞭解新的Java API及其特性,甚至用來進行復雜代碼的原型設計。這與枯燥的編輯、編譯和執行代碼的繁瑣過程不太一樣:

寫一個完整的程序;
編譯並修復錯誤;
運行程序;
弄清楚它有什麼問題;
修改;
重複這個過程。
除了IDE之外,現在還可以使用從Java SE 9以就隨ava SE JDK一起發佈的JShell。

什麼是JShell

現在,Java有了自己的REPL(Read-Evaluate-Print-Loop)實現JShell(Java Shell),作爲交互式的編程環境。那麼,它有什麼神奇的地方?JShell提供了一個快速友好的環境,讓你能夠快速探索、發現和試驗Java語言特性及其豐富的庫。
在JShell中,你可以一次輸入一個程序元素,並可以立即看到結果,然後根據需要對代碼做出調整。因此,JShell用它的Read-Evaluate-Print循環取代了編輯、編譯和執行的繁瑣過程。在JShell中,你不需要編寫完整的程序,只需要編寫JShell命令和Java代碼片段即可。

當你輸入代碼段時,JShell會立即讀取、執行並打印結果,然後準備好執行下一個代碼片段。因此,JShell的即時反饋可以讓你保持注意力,提高你的效率,並加快學習和軟件開發過程。

對JShell的介紹就到此爲止(InfoQ最近對這個工具進行過全面介紹)。爲了深入瞭解JShell的功能,我錄製了一套視頻教程“Hands-on Java 10 Programming with JShell”,可以幫助你掌握JShell,可以從Packt或Udemy訪問這些教程。

現在,讓我們通過一些簡單的示例(使用JShell)來了解這個新的var類型能做些什麼。

必備軟件
爲了能用上JShell,我假設你安裝了Java SE或JDK 10+,並且JDK的bin目錄已經加入到系統路徑中。如果還沒有安裝,可以在這裏下載JDK 10+最新版本。

啓動JShell會話

在Windows上,打開命令提示符,輸入jshell並按回車鍵。
在Linux上,打開一個shell窗口,輸入jshell並按回車鍵。
在macOS(以前稱爲OS X)上,打開終端窗口,輸入“jshell”並按回車鍵。
這個命令會啓動一個新的JShell會話,並顯示這個消息:

| Welcome to JShell -- Version 10.0.1
| For an introduction type: /help intro
jshell>

使用“var”類型
現在你已經安裝了JDK 10,現在讓我們開始玩JShell。我們直接跳到終端,通過示例來了解var類型。只需在jshell提示符下輸入我接下來要介紹的每個代碼片段,我會把結果留給你作爲練習。如果你稍微有瞄過一兩眼在代碼,你會注意到它們看起來好像是錯的,因爲當中沒有分號。你可以試試看,看看能不能運行。

簡單的類型推理
這是var類型的基本用法,在下面的示例中,編譯器可以將RHS推斷爲String字面量:

var name = "Mohamed Taman"
var lastName = str.substring(8)
System.out.println("Value: "+lastName +" ,and type is: "+ lastName.getClass().getTypeName())

這裏不需要分號,因爲JShell是一個交互式環境。只有當同一行代碼有多個語句或一個類型聲明或方法聲明中有多個語句時才需要分號,你將在後面的示例中看到。

var類型和繼承
在使用var時,多態仍然有效。在繼承的世界中,var類型的子類型可以像平常一樣賦值給超類型的var類型,如下所示:

import javax.swing.*
var password = new JPasswordField("Password text")
String.valueOf(password.getPassword()) // // 將密碼的字符數組轉換成字符串
var textField = new JTextField("Hello text")
textField = password
textField.getText()

但不能將超類型var賦值給子類型var,如下所示:

password = textField

這是因爲JPasswordField是JTextField的子類。

var和編譯時安全性
如果出現錯誤的賦值操作會怎樣?不兼容的變量類型不能相互賦值。一旦編譯器推斷出實際類型的var,就不能將錯誤的值賦值給它,如下所示:

var number = 10
number = "InfoQ"

這裏發生了什麼?編譯器將“var number = 10”替換爲“int number = 10”,所以仍然可以保證安全性。

var與集合和泛型
現在讓我們來看看var與集合和泛型一起使用時如何進行類型推斷。我們先從集合開始。在下面的情況中,編譯器可以推斷出集合元素的類型是什麼:

var list = List.of(10);

這裏沒有必要進行類型轉換,因爲編譯器已經推斷出正確的元素類型爲int。

int i = list.get(0); //等效於: var i = list.get(0);

下面的情況就不一樣了,編譯器只會將其作爲對象集合(而不是整數),因爲在使用菱形運算符時,Java需要LHS(左側)的類型來推斷RHS的類型:

var list2 = new ArrayList<>(); list2.add(10); list2
int i = list2.get(0) //編譯錯誤
int i = (int) list2.get(0) //需要進行轉換,獲得int

對於泛型,最好在RHS使用特定類型(而不是菱形運算符),如下所示:

var list3 = new ArrayList<Integer>(); list3.add(10); System.out.println(list3)
int i = list3.get(0)

for循環中的var類型
讓我們先來看看基於索引的For循環:

for (var x = 1; x <= 5; x++) {
var m = x 2; //等效於: int m = x 2;
System.out.println(m);
}

下面是在For Each循環中:

var list = Arrays.asList(1,2,3,4,5,6,7,8,9,10)
for (var item : list) {
var m = item + 2;
System.out.println(m);
}

現在我有一個問題,var是否適用於Java 8 Stream?讓我們看看下面的例子:

var list = List.of(1, 2, 3, 4, 5, 6, 7)
var stream = list.stream()
stream.filter(x -> x % 2 == 0).forEach(System.out::println)

var類型和三元運算符
那麼三元運算符呢?

var x = 1 > 0 ? 10 : -10
int i = x

現在,如果在三元運算符的RHS中使用不同類型的操作數會怎樣?讓我們來看看:

var x = 1 > 0 ? 10 : "Less than zero"; System.out.println(x.getClass()) //Integer
var x = 1 < 0 ? 10 : "Less than zero"; System.out.println(x.getClass()) // String

這兩個例子是否可以說明var的類型是在運行時決定的?絕對不是!讓我們以舊方式實現同樣的邏輯:

Serializable x = 1 < 0 ? 10 : "Less than zero"; System.out.println(x.getClass())

Serializable是其中兩個操作數最具兼容性和最專的有類型(最不專有的類型是java.lang.Object)。

String和Integer都實現了Serializable。Integer從int自動裝箱。換句話說,Serializable是兩個操作數的LUB(最小上限)。所以,這表明往前數第三個例子中的var類型也是Serializable。

讓我們轉到另一個主題:將var類型傳給方法。

var類型與方法
我們先聲明一個名爲squareOf的方法,這個方法的參數爲BigDecimal類型,並返回參數的平方,如下所示:

BigDecimal squareOf(BigDecimal number) {
var result= number.multiply(number);
return result;
}
var number = new BigDecimal("2.5")
number = squareOf(number)

現在讓我們看看它如何與泛型一起使用。我們聲明一個名爲toIntgerList的方法,參數類型爲List<T>(泛型類型),並使用Streams API返回一個整數列表,如下所示:

<T extends Number> List<Integer> toIntgerList(List<T> numbers) {
               var integers = numbers.stream()
                                    .map(Number::intValue)
                                    .collect(Collectors.toList());
               return integers;
}

var numbers = List.of(1.1, 2.2, 3.3, 4.4, 5.5)
var integers = toIntgerList(numbers)

var類型與匿名類
最後,讓我們看一下var和匿名類。我們通過實現Runnable接口來使用線程,如下所示:

<T extends Number> List<Integer> toIntgerList(List<T> numbers) {
               var integers = numbers.stream()
                                    .map(Number::intValue)
                                    .collect(Collectors.toList());
               return integers;
}

var numbers = List.of(1.1, 2.2, 3.3, 4.4, 5.5)
var integers = toIntgerList(numbers)

到目前爲止,我已經介紹了Java 10的新特性——“var”類型,它減少了樣板編碼,同時保持了Java的編譯時類型檢查。我還通過實例說明了可以用它做些什麼。接下來,你將瞭解var類型的侷限性以及不能將它用在哪些地方。

var message = "running..." //effectively final
         var runner = new Runnable(){
                  @Override
                  public void run() {
                           System.out.println(message);
                  }}

runner.run()

“var”的侷限性
接下來,你將看一些示例,以便了解var類型功能無法做到的事情。

jshell提示符將會告訴你代碼出了什麼問題,你可以利用這些交互式的即時反饋。

應該要進行初始化
第一個也是最簡單的原則就是不允許沒有初始值的變量。

var name;

你將得到一個編譯錯誤,因爲編譯器無法推斷這個局部變量x的類型。

不允許複合聲明
嘗試運行這行代碼:

var x = 1, y = 3, z = 4

你將得到一個錯誤消息:複合聲明中不允許使用’var’。

不支持確定性賦值(Definite Assignment)
嘗試創建一個名爲testVar的方法,如下所示,將下面的代碼複製並粘貼到JShell中:

void testVar(boolean b) {
var x;
if (b) {
x = 1;
} else {
x = 2;
}
System.out.println(x);
}

方法不會被創建,而是會拋出編譯錯誤。因爲沒有設置初始值,所以不能使用’var’。
null賦值
不允許進行null賦值,如下所示:

var name = null;

這將拋出異常“variable initializer is ‘null’”。因爲null不是一個類型。

與Lambda一起使用
另一個例子,沒有Lambda初始化器。這與菱形操作符那個示例一樣,RHS需要依賴LHS的類型推斷。

var runnable = () -> {}

將拋出異常:“lambda expression needs an explicit target-type”。

var和方法引用
沒有方法引用初始值,類似於Lambda和菱形運算符示例:

var abs = BigDecimal::abs

將拋出異常:“method reference needs an explicit target-type”。

var和數組初始化
並非所有數組初始化都有效,讓我們看看什麼時候var與[]不起作用:

var numbers[] = new int[]{2, 4, 6}

以下也不起作用:

var numbers = {2, 4, 6}

拋出的錯誤是: “array initializer needs an explicit target-type”。

就像上一個例子一樣,var和[]不能同時用在LHS一邊:

var numbers[] = {2, 4, 6}

錯誤: ‘var’ is not allowed as an element type of an array。

只有以下數組初始化是有效的:

var numbers = new int[]{2, 4, 6}
var number = numbers[1]
number = number + 3

不允許使用var字段

class Clazz {
private var name;
}

不允許使用var方法參數

void doAwesomeStuffHere(var salary){}

不能將var作爲方法返回類型

var getAwesomeStuff(){ return salary; }

catch子句中不能使用var

try {
Files.readAllBytes(Paths.get("c:\temp\temp.txt"));
} catch (var e) {}

在編譯時var類型究竟發生了什麼?
“var”實際上只是一個語法糖,並且它不會在編譯的字節碼中引入任何新的結構,在運行期間,JVM也沒有爲它們提供任何特殊的指令。

結論

在這篇文章中,我介紹了“var”類型是什麼以及它如何減少樣板編碼,同時保持Java的編譯時類型檢查。

然後,你瞭解了新的JShell工具,即Java的REPL實現,它可以幫助你快速學習Java語言,並探索新的Java API及其功能。你還可以使用JShell對複雜代碼進行原型設計,而不是重複編輯、編譯和執行的傳統繁瑣流程。

最後,你瞭解了所有var類型的功能和限制,例如什麼時候可以和不可以使用var。寫這篇文章很有意思,所以我希望你喜歡它並能給你帶來幫助。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章