Java學習日常——運行時類型信息和反射

附上思維導圖。這篇博客雖然不是完全按照這份思維導圖寫的,但是主要講了以下的知識點。

這裏寫圖片描述


在《Thinking in Java》的第十四章類型信息中,提到了運行時類型鑑別(Run-Time Type Identification, RTTI)。其實RTTI的說法是源自於C++的,在C++中通過RTTI可以得到基類指針或引用所指對象的實際類型。

在Java中,有叫做反射(Reflection)的機制。它不僅可以完成運行時類型鑑別,還可以在運行時獲得對象和類型的信息,而且還能動態加載類,動態訪問以及調用目標類型的字段,方法以及構造函數。

看書的時候總是給這倆名詞給搞混掉。但其實反射和RTTI就是Java和C++在同一件事情上的兩種描述而已。只是《Think in Java》的作者先學的C++,在寫書的時候把RTTI給搬進來了,書中的傳統的RTTI指的其實就是C++中的RTTI吧。

看了前面三段話,大家可能會想,啥是RTTI啊,有啥用啊,動態加載類又是個啥玩意……。之後我們會從爲什麼要有RTTI開始,一點一點講這些個知識點。


爲什麼需要RTTI

個人觀點,RTTI是爲了補償多態機制而存在的。多態機制要求我們儘量編寫家族通用的代碼。至於爲什麼要編寫家族通用的代碼,請見下一段。

假設我們有一個Animal家族,這個家族裏有Dog類Cat類Mouse類等等的各種各樣的動物類,這些類都繼承自Animal類,從倫理上來講,這些個Dog啊,Cat啊,同時也是AnimalAnimal能有的行爲(方法),它們也都有。爲了編寫家族通用的代碼,我們總是用Animal作爲這個家族的代表,將其他對象交給Animal引用,然後通過Animal引用來訪問這些個對象。下面見一段跑不通的代碼。

public class AnimalRestaurant{

    public static void serveAnimal(Animal hungryAnimal){
        ...各種準備工作...

        // 由於多態機制,會根據hungryAnimal引用的對象類型,調用相應的eat(food)方法
        dirtyAnimal.eat(food); 

        ...付錢走人...
    }

}

在上面這一段代碼中,我們開了一家動物餐廳,專門爲動物服務。按上面這種寫法,那我們這個世界的動物要去餐廳吃飯時,不管是什麼動物,只要調用AnimalRestaurant.serveAnimal(Animal hungryAnimal)這個方法就行了。而不用爲每個動物都寫一個方法,像什麼serveDogserveCat都不需要。所以代碼的重用性就很高,一份代碼家族裏的所有類都能用。而且代碼的耦合度很低,因爲動物的eat行爲是動物自己定的,不是我們寫死的。所以代碼也易擴展,有新動物來了,只要多加一個動物類就好了,這份代碼根本不用動,因爲是家族通用的。……

其實更專業一點地講,多態要我們編寫泛化的代碼。代碼要儘量少地關心對象的具體類型,所以在編寫代碼的時候,我們主動丟失了對象的類型信息。但有時我們就是需要知道對象的具體類型,比如上面動物餐廳例子中,我們可能需要統計今天到店裏來的每種動物的數量,來決定之後的經營方針。所以我們需要RTTI,假如沒有多態,那麼這個對象的類型就一目瞭然了,也就美RTTI的事了。


RTTI在Java中的具體應用

Java中用到RTTI的地方有這麼幾個。

  1. 向下轉型時的類型安全檢查。在將基類引用指向的對象轉型爲子類型,Java會獲取這個對象的具體類型,並檢查這個轉型是否正確,如果不正確,就拋出一個ClassCastException異常。
  2. 關鍵字instanceof。通過instanceof關鍵字,我們可以判斷一個對象是否是某個類型的實例。如x instanceof Dog,就可以判斷x指向的對象是否是一個Dog類的實例。
  3. Reflection API。通過Reflection API,我們可以在運行時得到類型和對象的類型信息,而且還能動態加載類,動態訪問類型的方法,字段。
  4. 上面說的都是Java語言提供給編程者使用的RTTI功能。如果把RTTI作爲一種在運行時獲取類型信息的思想,那麼多態機制也是通過RTTI實現的,在運行時根據對象的具體類型調用相應的方法。

接下來,我們主要會講Reflection API。前兩項都比較簡單,就略過了。


反射

通過反射我們可以在運行時獲取類型信息,像是類的名字,方法,字段,此外還可以用反射來創建一個新對象,訪問其中的方法,字段。Java的反射特殊在可以在編譯時不知道這個類的情況下來使用這個類。

在Reflect API中,有幾個主要的類。

  1. Class類。對應類。
  2. Constructor類。對應類的構造方法。
  3. Method類。對應類中的方法。
  4. Field類。對應類中的字段。
  5. Annotation類。對應註解。
  6. Array類。對應數組。

關於反射的使用可以在如下網址中學習,思路很乾淨很清晰。在這裏只介紹幾個知識點。

Java Reflection Tutorial

運行時類型信息的表示

一個類的類型信息在運行時,可以通過它的Class對象來訪問。什麼意思呢?就是說你寫了一個類,裏面有各種方法啊字段啊,在運行時,這個類的這些個方法啊字段啊,以及類的名字啊,它爸是誰啊什麼的這些類型信息都可以通過這個類的Class對象訪問。

每一個類都有一個Class對象,注意類的Class對象不是該類的實例對象哇。每當編寫並且編譯了一個新類,就會生成一個同名的.class文件,其中保存了一個類的類型信息。而當Java程序運行需要某個類時,JVM會通過被稱爲類加載器的子系統從.class文件中載入類型信息,並在堆中新建一個該類的Class對象,通過這個對象可以獲取這個類的類型信息。

Java代碼的動態加載機制

所有的類都是在對其第一次使用時,動態加載到JVM(Java Virtual Machine)中的。注意,這是一個很有逼格的小知識點!

我們從程序的啓動開始。我們先編寫了如下的一個Launcher類,它的main函數裏面還使用了一個Rocket類。

    public class Launcher {
        public static void main(String[] args) {
            System.out.println("Program start!");
            Rocket rocket = new Rocket();
            ...
        }       }

然後我們以指令javac Launcher.java進行編譯,生成一個.class文件。之後,就通過指令java Launcher嘗試調用Launcher類的main方法,執行時,JVM發現需要用到Launcher類,所以它通過讀取.class文件加載了Launcher類,並在堆中生成了Class對象,然後才找到了它的main方法開始了程序。

這個時候,Rocket並沒有被加載到JVM中,直到執行到new Rocket,JVM纔去加載它。所以說Java的代碼是動態加載的。Java的這種機制可以實現一些很炫酷的功能,像Android中的插件化,其實就是利用了java代碼動態加載的特點,實現了類似在汽車跑起來的時候給他換輪胎這樣的不可思議功能,又比如分佈式計算啊,IDE的可視化編程方法都需要在運行時加入編譯時未知的類,這些編譯時未知的類可以用反射在運行時獲取她們的類信息,並調用這些類的實例對象的方法!

動態加載是Java的一大特點,像C++的代碼,是在運行前就全部加載好的。你可能會問C++爲什麼不支持動態加載呢?因爲C++不像Java,它沒有JVM來中轉這些代碼。私以爲Java和C++最大的區別就是Java多了一個虛擬機。

Got it?


兩個RTTI

個人覺得應該有兩個RTTI。一個是C++中的運行時類型鑑別(Run-Time Type Identification, RTTI)。另一個是面向對象編程(Object Oriented Programming, OOP)語言中都會有的在運行時獲取類型信息的功能,運行時類型信息(Run-Time Type Information)。後者是一個抽象類,前者是C++對這個類的一個實現。

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