之前看java編程思想第十四章類型信息,直接跳過了對RTTI概念的理解,只看了class、instanceof等的用法,發現這樣的做法是不可取的,因爲這樣就只是會用這些提供的類而忽略了真正的原理。
RTTI假定我們在編譯時已經知道了所有類型,然後RTTI才能在運行時進行正確性檢查,這裏是比較容易誤解的地方,舉個例子:
public class RuntimeClass {
public void test(){
List runtime = new LinkedList();
runtime.add(new RuntimeClass());
String runtimeStr = (String) runtime.get(0);
runtimeStr.toString();
}
public static void main(String[] args){
RuntimeClass runtimeclass = new RuntimeClass();
runtimeclass.test();
}
}
上圖中我們先向list放入RuntimeClass類型的對象,然後通過強制轉換變成String類型,在編譯時成功生成class文件並且不報錯。但是當我們運行main函數的時候,拋出了ClassCastException,這就說明了RTTI是在運行時才進行類型的正確性檢查。在上圖中模糊了list的類型(默認爲object),舉個不恰當的例子在編譯的時候編譯器只會看你對象設置的範圍是否大於你要轉換的範圍,如果你要轉換的類型在你對象設置的範圍內,則編譯通過,上面的例子還有一個小細節,向上轉型的時候不需要顯示的寫出轉換類型而向下轉型的時候卻要顯示的類型,這是因爲編譯器知道向上轉型的類型就是你要轉的類型 而向下轉型的類型是不確定的,所以編譯器需要你顯示的寫出轉換類型以便編譯器進行檢查是否合理。其實這是向下轉型的一種情況,只是先做了向上轉型成Object類型,然後在向下轉型時編譯器識別object可以轉換成Runtime(由於在編譯器還拿不到實際引用的類型信息所以只能對顯示類型進行檢查),但是在運行時發現實際類型不對應才報錯。
public void test(){
RuntimeClass runtimeClass = (RuntimeClass)"test";
}
如果像上圖一樣,直接進行不同類型的強制轉換,編譯肯定是不能通過的,因爲兩者類型沒有任何關係,既不是向上轉型也不是向下轉型所以在編譯期就會檢查出來。
介紹了概念我們來介紹下類型信息的使用:
class對象
通過類加載器jvm爲每個類生成一個class對象保存在.class文件中,這裏的class對象就包含了與類有關的信息。class對象並不是一開始就存在的,而是在這個類第一次被靜態引用(調用構造器,構造器默認是靜態的)時動態加載到jvm中,此class對象被用於所有目標類對象的創建。
class對象有幾種方式獲取:
1、getClass()方法
這種方式需要你擁有目標類的對象,runtime.getClass();這個方法是Object中的方法,它返回的是目標類的class對象。
2、 Class.forName()
Class.forName()是Class類的一個靜態方法,它並不需要目標類的對象即可加載類,一般用來靜態語句的初始化; 它的輸入是一個包含目標類名的字符串,返回的是目標類的引用;如果找不到指定路徑的類,這個方法會拋出ClassNotFoundException。
這裏額外介紹下ClassNotFoundException和NoClassDefFoundError的區別:
ClassNotFoundException是在使用常見的三種方式(class類的forName方法、classloader類中的findSystemClass方法、classloader類中的loadclass方法)加載類的時候找不到需要加載的類,從名字可以看出這個是異常可以被捕獲處理;而NoClassDefFoundError則是編譯時能夠加載類,運行時發現類不見了屬於錯誤;
public class RuntimeClass {
class Circle extends RuntimeClassInterface{
}
public void test(){
try {
Class runtimeClass = Class.forName("Circle");
}catch (ClassNotFoundException e){
System.out.println("Not found Class !");
}
}
}
3、類字面常量
public void test(){
Class runtimeClass =RuntimeClass.class;
}
和forName、getClass的方式相比這種方式簡單,而且更安全,因爲他在編譯時就會受到檢查(因此不需要至於try語句塊中),也不需要調用任何方法,生成任何對象,所以效能也更高。
類的初始化:
static初始化是在類加載的時候進行的,通俗理解一下就是static塊或者static屬性只有在類第一次加載時纔會被執行;
類字面常量這種方式不會自動初始化目標類,而forName爲了得到class引用會先初始化目標類。如果一個static final值是“編譯器常量”那麼這個值不需要初始化就可以被讀取,反過來讀取這個值也不會引起類的初始化加載;如果將一個域(值是在第一次加載時才確定)設置成static final是不能確保這種行爲的。如果一個static不是final,那麼在讀取之前要求先進行鏈接和初始化。
泛化的class引用
爲了使class的引用更加具體可以通過泛型進行類型限制class<RuntimeClass>,也可以通過Class<?>通配符來放鬆限制,通過泛型可以在編譯器檢查類型正確性。也可以組合使用 Class<? extends RuntimeClass>.
instanceof用於特定類型判斷
public void test(){
RuntimeClass runtimeClass = new RuntimeClass();
if(runtimeClass instanceof RuntimeClass){
System.out.println(" This is true !");
}
}
動態instanceof
public void test(){
Class runtimeClass =RuntimeClass.class;
if( runtimeClass.isInstance(new RuntimeClass()) ){
System.out.println(" This is true !");
}
if( runtimeClass.isInstance( RuntimeClass.class) ){
System.out.println(" This is true !");
}
instanceof和Class的區別
instanceof在判斷類型的時候範圍是這個類的或者這個類的派生類,而用class進行==判斷時 ,只判斷是不是這個類不會判斷是不是這個類的派生類。
反射
前面介紹的是在編譯器已知的情況下,RTTI可以告訴你類型信息、檢查你類型轉換的正確性,但是在有些特殊情況下,在編譯時無法得知對象所屬的類。反射提供了這種可能,通過反射告知jvm類信息。RTTI和反射的區別在於,RTTI可以在編譯的時候打開和檢查.class文件,而對於反射來說編譯時獲取不到.class文件,只能在運行時打開和檢查。java通過class和java.lang.reflect實現反射。
public void test()throws Exception{
Class c = Class.forName("RuntimeClass");
Field field = c.getField("");
Constructor constructor = c.getConstructor(RuntimeClass.class);
Method method = c.getMethod("");
}
類型信息的應用 動態代理
由於java中存在三種形式的動態代理內容較多,所以放在《動態代理擴展》中講解。
關於類型信息就介紹到這。歡迎評論指正寫的不好的地方。