我反編譯了Java 10的本地變數型別推斷

HollisChuang發表於2019-01-18

北京時間 3 月 21 日,Oracle 官方宣佈 Java 10 正式釋出。這是 Java 大版本週期變化後的第一個正式釋出版本。關於Java 10 ,最值得程式設計師關注的一個新特性恐怕就是本地變數型別推斷(local-variable type inference)了。

Java 10 推出之後,很多文章也隨之出來了,告訴我們有哪些特性,告訴我們本地變數型別推斷怎麼用。但是,知其然,要知其所以然。

Java 10釋出之後,我第一時間下載了這個版本的Jdk並安裝到我的電腦中,然後寫了一段程式碼,真正的感受一下本地變數推斷到底如何。這篇文章簡單來談一下我的感受。

關於本地變數型別推斷的用法,我的《Java 10將於本月釋出,它會改變你寫程式碼的方式》中有介紹過。主要可以用在以下幾個場景中:

public class VarDemo {

    public static void main(String[] args) {
        //初始化區域性變數  
        var string = "hollis";
        //初始化區域性變數  
        var stringList = new ArrayList<String>();
        stringList.add("hollis");
        stringList.add("chuang");
        stringList.add("weChat:hollis");
        stringList.add("blog:http://www.hollischuang.com");
        //增強for迴圈的索引
        for (var s : stringList){
            System.out.println(s);
        }
        //傳統for迴圈的區域性變數定義
        for (var i = 0; i < stringList.size(); i++){
            System.out.println(stringList.get(i));
        }
    }
}
複製程式碼

然後,使用java 10的javac命令進行編譯:

/Library/Java/JavaVirtualMachines/jdk-10.jdk/Contents/Home/bin/javac VarDemo.java
複製程式碼

生成VarDemo.class檔案,我們對VarDemo.class進行反編譯。用jad進行反編譯得到以下程式碼:

public class VarDemo
{
    public static void main(String args[])
    {
        String s = "hollis";
        ArrayList arraylist = new ArrayList();
        arraylist.add("hollis");
        arraylist.add("chuang");
        arraylist.add("weChat:hollis");
        arraylist.add("blog:http://www.hollischuang.com");
        String s1;
        for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(s1))
            s1 = (String)iterator.next();

        for(int i = 0; i < arraylist.size(); i++)
            System.out.println((String)arraylist.get(i));

    }
}
複製程式碼

這段程式碼我們就很熟悉了,就是在Java 10之前,沒有本地變數型別推斷的時候寫的程式碼。程式碼的對應關係如下:

本地變數型別推斷寫法 正常寫法
var string = "hollis"; String string = "hollis";
var stringList = new ArrayList(); ArrayList stringList = new ArrayList();
for (var s : stringList) for (String s : stringList)
for (var i = 0; i < stringList.size(); i++) for (int i = 0; i < stringList.size(); i++)

ArrayList arraylist = new ArrayList(); 其實是ArrayList<String> stringList = new ArrayList<String>(); 解糖後,型別擦除後的寫法。

for(Iterator iterator = arraylist.iterator(); iterator.hasNext(); System.out.println(s1)) 其實是 for (String s : stringList) 這種for迴圈解糖後的寫法。

所以,本地變數型別推斷,也是Java 10提供給開發者的語法糖。雖然我們在程式碼中使用var進行了定義,但是對於虛擬機器來說他是不認識這個var的,在java檔案編譯成class檔案的過程中,會進行解糖,使用變數真正的型別來替代var(如使用String string 來替換 var string)。對於虛擬機器來說,完全不需要對var做任何相容性改變,因為他的生命週期在編譯階段就結束了。唯一變化的是編譯器在編譯過程中需要多增加一個關於var的解糖操作。

感興趣的同學可以寫兩段程式碼,一段使用var,一段不使用var,然後對比下編譯後的位元組碼。你會發現真的是完全一樣的。下圖是我用diff工具對比的結果。

diff

語法糖(Syntactic Sugar),也稱糖衣語法,是由英國計算機學家 Peter.J.Landin 發明的一個術語,指在計算機語言中新增的某種語法,這種語法對語言的功能並沒有影響,但是更方便程式設計師使用。簡而言之,語法糖讓程式更加簡潔,有更高的可讀性。

和JavaScript有啥區別

很多人都知道,在JavaScript中,變數的定義就是使用var來宣告的。所以,Java 10的本地變數型別推斷剛剛一出來,就有人說了,這不就是抄襲JavaScript的嗎?這和JS裡面的var不是一樣嗎?

其實,還真的不一樣。

首先,JavaScript 是一種弱型別(或稱動態型別)語言,即變數的型別是不確定的。你可以在JavaScript中,使用“4”-3這樣的語法,他的的結果是數字1,這裡字串和數字做運算了。不信的話,你開啟你瀏覽器的控制檯,試一下:

console

但是,Java中雖然可以使用var來宣告變數,但是它還是一種強型別的語言。通過上面反編譯的程式碼,我們已經知道,var只是Java給開發者提供的語法糖,最終在編譯之後還是要將var定義的物件型別定義成編譯器推斷出來的型別的。

到底會不會影響可讀性

本地變數型別推斷最讓人詬病的恐怕就是其可讀性了,因為在之前,我們定義變數時候要明確指定他的型別,所以在閱讀程式碼的時候只要看其宣告的型別就可以知道他的型別了,但是全都使用var之後,那就慘了。毫無疑問,這會損失一部分可讀性的。但是,在程式碼中使用var宣告物件同樣也帶來了很多的好處,如程式碼更加簡潔等。

一個新東西剛剛出來之前,總會有各種不習慣。現在大家就會覺得這東西太影響我閱讀程式碼的效率。就像淘寶商城剛剛改名叫天貓的時候,大家都覺得,這是個什麼鬼名字。現在聽習慣了,是不是覺得還挺好的。

如果大家都使用了var來宣告變數以後,那麼變數的名字就更加重要了。那時候大家就會更注重變數起名的可讀性。而且,相信不久,各大IDE就會推出智慧顯示變數的推斷型別功能。所以,從各個方面,都能彌補一些不足。

總之,對於本地變數型別推斷這一特性,我是比較積極的擁抱的。

最後,再提出一個問題,供大家思考,本地變數型別推斷看上去還是挺好用的,而且,既然Java已經決定在新版本中推出他,那麼為什麼要限制他的用法呢。現在已知的可以使用var宣告變數的幾個場景就是初始化區域性變數、增強for迴圈的索引和傳統for迴圈的區域性變數定義,還有幾個場景是不支援這種用法的,如:

方法的引數 建構函式的引數 方法的返回值型別 物件的成員變數 只是定義定義而不初始化

那麼,我的問題是,Java為什麼做這些限制,考慮是什麼?

我反編譯了Java 10的本地變數型別推斷

相關文章