在本文中,我將通過示例介紹新的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並按Enter鍵。 在Linux上,開啟一個shell視窗,輸入jshell並按Enter鍵。 在macOS(以前稱為OS X)上,開啟終端視窗,輸入“jshell”並按Enter鍵。 這個命令會啟動一個新的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(泛型型別),並使用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。寫這篇文章很有意思,所以我希望你喜歡它並能給你帶來幫助。