深入理解Java類型信息(Class對象)與反射機制
深入理解Class對象
RRTI的概念以及Class對象作用
認識Class對象之前,先來了解一個概念,RTTI(Run-Time Type Identification)運行時類型識別,對於這個詞一直是 C++ 中的概念,至於Java中出現RRTI的說法則是源於《Thinking in Java》一書,其作用是在運行時識別一個對象的類型和類的信息,這裏分兩種:傳統的”RRTI”,它假定我們在編譯期已知道了所有類型(在沒有反射機制創建和使用類對象時,一般都是編譯期已確定其類型,如new對象時該類必須已定義好),另外一種是反射機制,它允許我們在運行時發現和使用類型的信息。在Java中用來表示運行時類型信息的對應類就是Class類,Class類也是一個實實在在的類,存在於JDK的java.lang包中,其部分源碼如下:
類被創建後的對象就是對象,註意,對象表示的是自己手動編寫類的類型信息,比如創建一個Shapes類,那麽,JVM就會創建一個Shapes對應類的對象,該對象保存了Shapes類相關的類型信息。實際上在Java中每個類都有一個對象,每當我們編寫並且編譯一個新創建的類就會產生一個對應對象並且這個對象會被保存在同名.class文件裏(編譯後的字節碼文件保存的就是對象),那為什麽需要這樣一個對象呢?是這樣的,當我們new一個新對象或者引用靜態成員變量時,Java虛擬機(JVM)中的類加載器子系統會將對應對象加載到JVM中,然後JVM再根據這個類型信息相關的對象創建我們需要實例對象或者提供靜態變量的引用值。需要特別註意的是,手動編寫的每個class類,無論創建多少個實例對象,在JVM中都只有一個對象,即在內存中每個類有且只有一個相對應的對象,挺拗口,通過下圖理解(內存中的簡易現象圖):
到這我們也就可以得出以下幾點信息:
類也是類的一種,與class關鍵字是不一樣的。
手動編寫的類被編譯後會產生一個Class對象,其表示的是創建的類的類型信息,而且這個Class對象保存在同名.class的文件中(字節碼文件),比如創建一個Shapes類,編譯Shapes類後就會創建其包含Shapes類相關類型信息的Class對象,並保存在Shapes.class字節碼文件中。
每個通過關鍵字class標識的類,在內存中有且只有一個與之對應的Class對象來描述其類型信息,無論創建多少個實例對象,其依據的都是用一個Class對象。
類只存私有構造函數,因此對應對象只能有JVM創建和加載
類的對象作用是運行時提供或獲得某個對象的類型信息,這點對於反射技術很重要(關於反射稍後分析)。
對象的加載及其獲取方式
對象的加載
前面我們已提到過,Class對象是由JVM加載的,那麽其加載時機是?實際上所有的類都是在對其第一次使用時動態加載到JVM中的,當程序創建第一個對類的靜態成員引用時,就會加載這個被使用的類(實際上加載的就是這個類的字節碼文件),註意,使用new操作符創建類的新實例對象也會被當作對類的靜態成員的引用(構造函數也是類的靜態方法),由此看來Java程序在它們開始運行之前並非被完全加載到內存的,其各個部分是按需加載,所以在使用該類時,類加載器首先會檢查這個類的Class對象是否已被加載(類的實例對象創建時依據Class對象中類型信息完成的),如果還沒有加載,默認的類加載器就會先根據類名查找.class文件(編譯後Class對象被保存在同名的.class文件中),在這個類的字節碼文件被加載時,它們必須接受相關驗證,以確保其沒有被破壞並且不包含不良Java代碼(這是java的安全機制檢測),完全沒有問題後就會被動態加載到內存中,此時相當於Class對象也就被載入內存了(畢竟.class字節碼文件保存的就是Class對象),同時也就可以被用來創建這個類的所有實例對象。下面通過一個簡單例子來說明Class對象被加載的時機問題(例子引用自Thinking www.yszx11.cn/ in Java):
}
}
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
在上述代碼中,每個類Candy、Gum、Cookie都存在一個static語句,這個語句會在類第一次被加載時執行,這個語句的作用就是告訴我們該類在什麽時候被加載,執行結果:
1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9
從結果來看,new一個Candy對象和Cookie對象,構造函數將被調用,屬於靜態方法的引用,Candy類的Class對象和Cookie的Class對象肯定會被加載,畢竟Candy實例對象的創建依據其Class對象。比較有意思的是
1
1
其中forName方法是Class類的一個static成員方法,記住所有的Class對象都源於這個Class類,因此Class類中定義的方法將適應所有Class對象。這裏通過forName方法,我們可以獲取到Gum類對應的Class對象引用。從打印結果來看,調用forName方法將會導致Gum類被加載(前提是Gum類從來沒有被加載過)。
方法
通過上述的案例,我們也就知道Class.forName()方法的調用將會返回一個對應類的Class對象,因此如果我們想獲取一個類的運行時類型信息並加以使用時,可以調用Class.forName()方法獲取Class對象的引用,這樣做的好處是無需通過持有該類的實例對象引用而去獲取Class對象,如下的第2種方式是通過一個實例對象獲取一個類的Class對象,其中的getClass()是從頂級類Object繼承而來的,它將返回表示該對象的實際類型的Class對象引用。
try{
//通過Class.forName獲取Gum類的www.jyz521.com Class對象
}
//通過實例對象獲取Gum的Class對象
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
註意調用forName方法時需要捕獲一個名稱為ClassNotFoundException的異常,因為forName方法在編譯器是無法檢測到其傳遞的字符串對應的類是否存在的,只能在程序運行時進行檢查,如果不存在就會拋出ClassNotFoundException異常。
字面常量
在Java中存在另一種方式來生成Class對象的引用,它就是Class字面常量,如下:
//字面常量的方式獲取Class對象
1
2
1
2
這種方式相對前面兩種方法更加簡單,更安全。因為它在編譯器就會受到編譯器的檢查同時由於無需調用forName方法效率也會更高,因為通過字面量的方法獲取Class對象的引用不會自動初始化該類。更加有趣的是字面常量的獲取Class對象引用方式不僅可以應用於普通的類,也可以應用用接口,數組以及基本數據類型,這點在反射技術應用傳遞參數時很有幫助,關於反射技術稍後會分析,由於基本數據類型還有對應的基本包裝類型,其包裝類型有一個標準字段TYPE,而這個TYPE就是一個引用,指向基本數據類型的Class對象,其等價轉換如下,一般情況下更傾向使用.class的形式,這樣可以保持與普通類的形式統一。
深入理解Java類型信息(Class對象)與反射機制