1. 程式人生 > >Java新特性:資料型別可以扔掉了?

Java新特性:資料型別可以扔掉了?

在很久很久以前,我們寫程式碼時要慎重的考慮變數的資料型別,比如下面這些: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600230531018-c3963fb7-d5c3-4c66-bf48-788cfa3b16b8.png#align=left&display=inline&height=382&margin=%5Bobject%20Object%5D&name=image.png&originHeight=763&originWidth=1154&size=68369&status=done&style=none&width=577) > **列舉**:儘管在 JDK 5 中增加了列舉型別,但是 Class 檔案常量池的 CONSTANT_Class_info 型別常量並沒有發生任何語義變化,仍然是代表一個類或介面的符號引用,沒有加入列舉,也沒有增加過“CONSTANT_Enum_info”之類的“列舉符號引用”常量。所以使用 enum 關鍵字定義常量,儘管從 Java 語法上看起來與使用 class 關鍵字定義類、使用 interface 關鍵字定義介面是同一層次的,但實際上這是由 Javac 編譯器做出來的假象,**從位元組碼的角度來看,列舉僅僅是一個繼承於 java.lang.Enum、自動生成了 values() 和 valueOf() 方法的普通 Java 類而已,因此列舉也歸為引用型別了**。 然而到了 JDK 10 時,我們就有了新的選擇,JDK 10 中新增了 `var` 區域性變數推斷的功能,使用它我們可以很 happy 的忘記資料型別這件事了,那它是如何使用的呢?接下來我們一起來看。 ### 1、使用對比 接下來我們就使用對比的方式,來體會一下 `var` 的作用。 #### 場景一:定義字串 **舊寫法:** ```java String str = "Hello, Java."; ``` **新寫法:** ```java var s = "Hello, Java."; ``` > PS:這裡的舊寫法指的是 JDK 10 之前的版本,而新寫法指的是 JDK 10 以後(包含 JDK 10)的版本。 #### 場景二:數值相加 **舊寫法:** ```java int num1 = 111; double num2 = 555.666d; double num3 = num1 + num2; System.out.println(num3); ``` > PS:當遇到不同型別相加時(`int`+ `double`)會發生資料型別向上轉型,因此 `num3` 就會升級為 `double` 型別。 **新寫法:** ```java var n1 = 111L; var n2 = 555.666; var n3 = n1 + n2; System.out.println(n3); ``` #### 場景三:集合 **舊寫法:** ```java List list = new ArrayList<>(); list.add("Hello"); list.add("Java"); ``` **新寫法:** ```java var list = new ArrayList<>(); list.add("Hello"); list.add("Java"); ``` #### 場景四:迴圈 **舊寫法:** ```java for (Object item : list) { System.out.println("item:" + item); } for (int i = 0; i < 10; i++) { // do something... } ``` **新寫法:** ```java for (var item : list) { System.out.println("item:" + item); } for (var i = 0; i < 10; i++) { // do something... } ``` #### 場景五:配合 Lambda 使用 **舊寫法:** ```java List flist = list.stream().filter(v -> v.equals("Java")).collect(Collectors.toList()); System.out.println(flist); ``` **新寫法:** ```java var flist = list.stream().filter(v -> v.equals("Java")).collect(Collectors.toList()); System.out.println(flist); ``` ### 2、優點分析 通過上面的示例我們可以看出, `var` 具備**兩個明顯的優點:提高了程式碼的可讀性和命名對齊**。 #### ① 提高了可讀性 我們在沒有使用 `var` 之前,如果型別的名稱很長就會出現下面的這種情況: ```java InternationalCustomerOrderProcessor> orderProcessor = createInternationalOrderProcessor(customer, order); ``` 當限定每行不能超過 150 個字元的話,變數名就會被推到下一行顯示,這樣整個程式碼的可讀性就變得很低。但當我們使用了 `var` 之後,程式碼就變成了這樣: ```java var orderProcessor = createInternationalOrderProcessor(customer, order); ``` 從上述的程式碼可以看出,當型別越長時,`var`(可讀性)的價值就越大。 #### ② 命名對齊 在不使用 `var` 時,當遇到下面這種情況,程式碼就是這樣的: ```java // 顯式型別 No no = new No(); AmountIncrease more = new BigDecimalAmountIncrease(); HorizontalConnection jumping =   new HorizontalLinePositionConnection(); Variable variable = new Constant(6); List names = List.of("Java", "中文社群"); ``` 在使用了 `var` 之後,程式碼是這樣的: ```java var no = new No(); var more = new BigDecimalAmountIncrease(); var jumping = new HorizontalLinePositionConnection(); var variable = new Constant(6); var names = List.of("Java", "中文社群"); ``` 從上述程式碼可以看出使用了 `var` 之後,命名對齊了,整個程式碼也變得更優雅了。 ### 3、使用規則 & 反例 `var` 的實現來自於 JEP 286 (改善提議 286),詳情地址 :[http://openjdk.java.net/jeps/286](http://openjdk.java.net/jeps/286) 從 JEP 286 的標題“區域性變數型別推斷”可以看出,`var` 只能用於區域性變數宣告,也就是說 `var` 必須滿足以下條件: - 它只能用於區域性變數上; - 宣告時必須初始化; - 不能用作方法引數和全域性變數(類變數)。 > PS:因為 `var` 的實現必須根據等會右邊的程式碼進行型別推斷,因此它不能被賦值 null 或不被初始化。 #### 反例一:未初始化和賦值 null ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600150205371-538f72e8-4919-42aa-930c-dfccee59f68e.png#align=left&display=inline&height=63&margin=%5Bobject%20Object%5D&name=image.png&originHeight=126&originWidth=405&size=5535&status=done&style=none&width=202.5) ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600142203326-1588d90a-3233-442a-afb1-7a67b673a722.png#align=left&display=inline&height=72&margin=%5Bobject%20Object%5D&name=image.png&originHeight=144&originWidth=359&size=8306&status=done&style=none&width=179.5) #### 反例二:中途型別更改 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600150417808-b6a3b74f-feb1-43a1-b5e3-a2d98b5db8a0.png#align=left&display=inline&height=80&margin=%5Bobject%20Object%5D&name=image.png&originHeight=160&originWidth=417&size=8873&status=done&style=none&width=208.5) #### 反例三:全域性變數 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600143667539-00a56a90-7320-4cff-bb06-5a2defc0302a.png#align=left&display=inline&height=167&margin=%5Bobject%20Object%5D&name=image.png&originHeight=334&originWidth=881&size=43211&status=done&style=none&width=440.5) #### 反例四:作為返回值 ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600143749681-b6c183a2-f68e-4d28-85be-65f3d9f5e8cd.png#align=left&display=inline&height=149&margin=%5Bobject%20Object%5D&name=image.png&originHeight=298&originWidth=584&size=23450&status=done&style=none&width=292) ### 4、原理分析 經過前面的使用我們對 `var` 已經有了初步的認識,但 `var` 的實現原理是什麼呢? 為了搞清楚它的原理,我們對下面的程式碼進行了編譯(使用命令 `javac MainTest.java`): ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600144241812-3a70a692-72db-4c2e-8246-2fba24005e50.png#align=left&display=inline&height=410&margin=%5Bobject%20Object%5D&name=image.png&originHeight=820&originWidth=911&size=108079&status=done&style=none&width=455.5) 然後我們再用反編譯工具開啟被編譯的類發現:`var` 竟然被替換成一個個確定的資料型別了,如下圖所示: ![image.png](https://cdn.nlark.com/yuque/0/2020/png/92791/1600144017724-82aacdfc-5d0b-4b0c-936f-7edb0f3f40c5.png#align=left&display=inline&height=391&margin=%5Bobject%20Object%5D&name=image.png&originHeight=782&originWidth=895&size=124669&status=done&style=none&width=447.5) 由此我們可以得出結論:**`var` 關鍵字的實現和它的名字密切相關, `var` 只是區域性型別推斷,它只會在 Java 編碼期和編譯期有效,當類被編譯為 class 檔案時,`var` 就會變成一個個確定的資料型別(通過推斷得出)。** 所以我們可以把 `var` 通俗的理解為 Java 的語法糖,使用它可以讓我們快速優雅的實現業務程式碼,但 `var` 在位元組碼層面是不存在的。 ### 總結 本文我們介紹了 `var`(區域性型別推斷)的使用,它可以用在區域性變數、 `for`、 `Lambda` 的變數宣告中,但不能用在全域性變數的宣告中,也不能用它作為方法的返回值,並且在宣告時一定要進行初始化(也不能賦值為 null)。使用 `var` 可以有效的提高程式碼的可讀性和命名對齊,它的實現原理,是在編譯期通過等號右側的程式碼進行型別推斷,然後再將 `var` 替換成確定的資料型別。