由之前動态代理的學習再次接觸到反射這個知識點,第二次接觸了是以做了一些稍微深入的了解。那麼,對于反射這部分的内容我打算分三篇部落格來總結。本篇部落格先對反射做一個大概的了解,包括反射有關的rtti、定義的了解以及涉及到的其他知識的簡介。
java之前我接觸反射這個知識,是在大話設計中的抽象工廠模式裡,通過反射+配置檔案來優化抽象工廠提高其應對需求變更的靈活性。當時對于反射的認知僅僅是它是一種技術,一種執行個體化對象的技術,一種執行個體化對象不依賴于寫死的代碼的技術。簡單的說就是,它是一種可以擺脫用new去執行個體化對象,顯然它應付與對象變換的能力是強大的。
其實,反射不論在什麼語言裡都是一樣的,隻不過實作的手段不一樣而已。之前對代理模式的深入學習過程中又遇到了反射,是以決定這次要一探究竟。
java可以在運作時加載、探知、使用編譯期間完全未知的class。再簡單一點說就是java可以在運作時獲得任一一個類的資訊、建構類的class對象(強調:不是該類的對象,是後面提高的類類型)、生成類執行個體、調用methods。這裡類的資訊包括它的基類、所實作的接口、方法等。
個人覺得這裡比較難了解的是“編譯期間完全未知”。是以,特别解釋一下。如下的代碼。首先,這個note類是不存在的,也就是說這段代碼有錯。分别執行兩個方法就可以看出分别了,其實方法2編譯時就無法通過,提示類是不能識别的類型,因為本來就不存在這個類。而方法1則時可以通過編譯,執行時能列印“方法執行中”。但是執行個體化時會報空指針的錯誤。這兩種不同時刻産生的錯誤就說明了“編譯期間完全未知”這個說法。
[java]
view plaincopyprint?
public class client {
public static void main(string[] args)
throws instantiationexception, illegalaccessexception, classnotfoundexception{
system.out.println("方法執行中!"); //列印說明進入該方法
//方法1:通過反射執行個體化類note
class c=class.forname("com.zjjreflect.note");
object o=c.newinstance();
//方法2:直接執行個體化類note
//object o=new note();
}
}
rtti是run-time type information 的縮寫,意思是運作時類型資訊。,rtti提供了運作時确定對象類型的方法。但是,rtti并不是一種技術,應該是一種概念。因為不同的語言對rtti的實作形式是不一樣的。簡單的說就是在程式運作時去獲得一個對象所對應的類的資訊。這麼說有點模糊,還是結合rtti在某種語言裡的實作來了解好一些。java 中就是主要有:類型轉換、類類型class、instanceof展現了rtti。
class是所有類和接口的抽象,比如類的名字、類實作的接口、方法、屬性等等。你可以通過某個對象的class對象來擷取類的資訊。這裡不做詳細介紹,後面有單獨的部落格補充。
類加載的過程就是将.class加載到記憶體中。這裡為什麼要提到classloader呢?因為反射的過程使用到了classloader,并且不同的類需要相對應的classloader來加載。也就是說classloader是和類是配對工作的,然後反射的特點卻是在運作時才會知道類的資訊
是以我們也要對這部分的内容作個了解。也是為了能更好、更深刻的對反射了解。java的classloader有四種分别為:
bootstrap classloader :引導(也稱為原始)類加載器。
extension classloader :擴充類加載器。
application classloader:應用程式類加載器。
user defined classloader:自定義類加載器。
總結:在程式運作時通過類類型class獲得目标類的資訊,然後在類資訊的基礎上使用相對應的類加載器加載到記憶體中,再然後對這個類中進行執行個體化,方法調用等的使用的整個過程。就是反射的詳細的說法!!
上面簡要的提了一下java反射機制中涉及到的一些相關知識,那麼classloader就是其中之一。緊接着就詳細的對classloader做一個相對深入的了解。作為了解需要知道的是,其實類類型(class class)是和classloader分不開的,因為classloader需要的資訊是由它提供的。類類型将在下一篇部落格介紹。
classloader是負責加載類的對象,作用是根據jvm請求提供的類資訊,将請求的類加載的記憶體中或者說加載到jvm中。另外,每一個類的class對象(注意class是類類型)都持有一個對應的classloader的引用。可以通過class對象的getclassloader()方法得到。類和它的classloader是對應的,是以類隻能通過它對應的classloader加載。
注意:數組類的 class 對象不是由類加載器建立的,而是由 java 運作時根據需要自動建立。數組類的類加載器由 class.getclassloader() 傳回,該加載器與其元素類型的類加載器是相同的;如果該元素類型是基本類型,則該數組類沒有類加載器。
jvm在運作時會産生三個classloader,bootstrap classloader、extension classloader和app classloader。
bootstrap classloader:是用c++編寫的,是jvm的内置加載器,它的名字是null。它用來加載核心類庫,即在lib下的類庫。做個實驗,首先,string類肯定是java的核心類,那我們就以它為例來看看:
public static void main(string[] args){
string a="x";
system.out.println(a.getclass().getclassloader());
}
我們通過代碼來獲得string加載對應的classloader的名字輸出的結果為null。
extension classloader:加載lib/ext下的類庫。
app classloader:加載classpath裡的類庫。
之前我們說過,每一個class對象都會持有一個對應的classloader的引用。每一個classloader對象也會持有一個parent classloader的引用。這裡需要特别注意的是:這裡所指的的parent classloader不是我們熟悉的繼承關系,不是父類!!首先,我們要知道這裡說的是classloader對象,也就是說這的parent
classloader其實是一個對象的引用。下面看一張圖,了解一下classloader對象之間的層次關系:
我們這裡可以在做一個實驗,來體會一下這個層次關系。代碼如下:
public static void main(string[] args){
classloader c =testclassloader.class.getclassloader();
do {
system.out.println(c.getclass().getname());
c=c.getparent();
}while(c!=null);
輸出的結果為:
sun.misc.launcher$appclassloader
sun.misc.launcher$extclassloader
層次關系中我們了解到了很重要的一點:加載器對象之間的引用關系。被引用的對象稱之為引用對象的父加載器,可以通過getparent()方法得到。那麼雙親加載機制就是基于這種引用的層次關系。即:當一個classloader接到請求時,它不是直接加載對應的類,而是詢問它引用的classloader是否能夠加載,而這個父classloader則會詢問自己的引用的classloader是否加載了該類。隻有當所有的父classloader都沒有加載該類時,這個最初的classloader才自己去加載申請的類。
很繞啊,文字說不清楚還是上圖吧!
雙親加載機制可以一定程度上保證安全性,因為隻要頂層classloader能加載的東西就一定不會讓下層的classloader有機會加載。也就保證了有些自定義的帶有破壞性的類不會被加載到jvm核心中。
結語:classloader相對難了解的地方一個是,對象将的層次關系和父加載器。另一個是雙親加載機制。這裡提供一個視訊供大家參考。
為了差別于我們常用的class,是以給了個中文名類類型。目的就是為了知道此class非彼class。前面已經介紹了class loader,它的作用是根據提供的資訊來加載類到記憶體中。我之前有提過這個類資訊的提供者就是本篇部落格要介紹的class。提醒:一個類的class對象和它的instance是不一樣的,切記,不然你會混亂的。開始了!
class類是所有類(注意是對象)的共有資訊的抽象,比如該類實作的接口、對應的加載器、類名等等。一句話,類類型儲存了每個類所對應的類型資訊。每一個類都有一個class對象,這個對象在類被加載後由jvm自動構造。也是由jvm管理的,class類是沒有公共的構造方法的。
class對象對于類來說就像是,dna對于每個人,裡面有你的一切生物資訊。java中可以通過class來取得類的執行個體,也許将來的将來通過你的dna也能得到你的另一個執行個體。科幻電影裡是已經實作了。ok,概念應該有個初步的認識了。
方法的介紹本來不應該這麼簡單,但是發現一句兩句的說不清楚,并且對于java的了解有很好的幫助。是以臨時決定這部分單獨的寫一篇部落格。這裡就簡單的列幾個,之前用過的方法。
forname:傳回與帶有給定字元串名的類或接口相關聯的 class 對象。
getname():一個class對象描述了一個特定類的屬性,class類中最常用的方法getname以 string 的形式傳回此 class 對象所表示的實體(類、接口、數組類、基本類型或 void)名稱。
newinstance():建立class對象描述的類型的新執行個體。newinstance()方法調用預設構造器(無參數構造器)初始化建立對象。
getclassloader():傳回該類的類加載器。
getinterfaces():确定此對象所表示的類或接口實作的接口。
getcomponenttype():傳回表示數組元件類型的 class。
getsuperclass():傳回表示此 class 所表示的實體(類、接口、基本類型或 void)的超類的 class對象
isarray():判定此 class 對象是否表示一個數組類。
獲得class對象的方法有三種
(1)利用object.getclass()方法擷取該對象的class執行個體;
(2)使用class.forname()靜态方法,用類的名字擷取一個class執行個體
(3)運用類的.class的方式來擷取class執行個體,對于基本資料類型的封裝類,還可以采用.type來擷取相對應的基本資料類型的class執行個體
這裡需要注意的是虛拟機隻會産生一份位元組碼, 用這份位元組碼可以産生多個執行個體對象。也就是說class對象隻會有一個。看如下代碼:
測試類
public class test {
static {
system.out.println("靜态初始化");
}
{
system.out.println("非靜态初始化");
用戶端
public static void main(string[] arg) throws classnotfoundexception, instantiationexception, illegalaccessexception{
//方法1
class c=class.forname("com.zjj.classtest.test");
//方法2
c=test.class;
//方法3
test t=new test();
c=t.getclass();
test t2=new test();
輸出結果為:
靜态初始化
非靜态初始化
大家知道靜态初始化方法是在類加載的時候執行的,非靜态初始化方法是在類被執行個體化的時候執行的。而輸出結果隻列印了一次“靜态初始化”,這就說明三次得到的class對象都是同一個。
也就是說,在運作期間,如果我們要産生某個類的對象或者的得到某個類的class對象,java虛拟機(jvm)會檢查該類型的class對象是否已被加載。如果沒有被加載,jvm會根據類的名稱找到.class檔案并加載它。一旦某個類型的class對象已被加載到記憶體,就可以用它來産生該類型的所有對象
本篇總結:至此,應該可以了解了class也是一個類,隻不過它是所有類的一個抽象,名字又和我們所知道的class一樣容易造成混淆。總的來說,每一個類都有對應的一個class對象來儲存這個類的資訊,這個class對象由jvm構造和管理。class對象的存在是java反射的基礎。
反射機制總結:反射機制是java的一個重要的内容,為java提供了運作時加載類的能力,也就是動态性。class是資訊提供者,class loader是加載工具,二者都是反射機制最基礎的部分。那麼所謂的反射就是解除耦合,方式就是通過class取得未知類的資訊,而後執行個體化。當然class loader的所做的工作是隐藏的,是class對象去調用的。是以無需顯示的自己調用。
反射機制這幾篇部落格寫下來發現涉及到java類的加載機制,這部分的内容也比較獨立的一部分,是以單另一篇來寫。在java中任何的類都是需要加載到jvm中才能運作的。之前class loader介紹了類的加載機制,那麼這裡要說的是不同加載方式之間的對比,好能對java類的執行個體化過程有更深刻的體會。
我們說代碼裡出現new關鍵字意味着對于可能變動的代碼,耦合過高了。遇到這種情況我們會用反射機制來去除new關鍵字,這在代理模式裡我們見過了。實際上也就是用了class.newinstance來代替。這說明這兩種方式都可以得到相同的對象執行個體,但是它們之間存在差別,耦合度不同。
實際上在了解上我們可以認為,class.newinstanc方式來執行個體化對象是對new關鍵字的拆分成兩步了。因為,class.newinstance的使用是有前提的,要保證類已經加載到jvm中,并且已經連結。看如下代碼:
<span style="font-family:fangsong_gb2312;font-size:18px;"><span style="font-family:fangsong_gb2312;">public static void main(string[] arg) throws classnotfoundexception, instantiationexception, illegalaccessexception{
//從目前線程取得正在運作的加載器
classloader cl=thread.currentthread().getcontextclassloader();
cl.loadclass("com.zjj.classtest.test"); //加載測試類到jvm
class c2=cl.getclass(); //得到類的class對象
c2.newinstance(); //執行個體化對象
}</span></span>
這裡不用class.forname來得到class對象是為了保證類被加載了但是沒有被連結。 這段代碼看着貌似沒什麼錯,編譯也沒有問題,但是運作的時候就出錯了。也就是說通過如上方法加載的類是沒有被連結的,是以newinstance方法無法執行。
前面說了解上可以簡單的認為是通過class.instance方式是new拆分的兩步,但是事實上new要比class.instance做的多。class.instance方法隻能通路無參數的構造函數,new則都可以通路。建立一個有兩個構造函數的測試類,看用戶端調用代碼:
class c=class.forname("com.zjj.classtest.test");
c.newinstance();
new test("ni");
輸出結果為:
無參數的構造函數
帶參數的構造函數
如果在newinstance中傳入參數去調用帶參數的構造函數的話是會報錯的,無法通過編譯。相對來說newinstance是弱類型,new是強類型。
講這兩個的差別之前我們先要了解,jvm會執行靜态代碼段,要記住一個概念,靜态代碼是和class綁定的,class裝載成功就表示執行了靜态代碼了,以後也就不會再走這段靜态代碼了。 也就是說靜态代碼段是隻會執行一次的,在類被加載的時候。另外我們還需要知道,類的加載過程分為裝載、連接配接、初始化。還有就是,jvm遇到類請求時它會先檢查記憶體中是否存在,如果不存在則去加載,存在則傳回已存在的class對象。
那麼這兩個方法的差別就在于執行的這三個過程不一樣。forname有兩個函數(多态),三個參數時forname(string classname, boolean initialize, classloader loader)第二個參數為true時則類會連結,會初始化。為false時,如果原來不存在則一定不會連接配接和初始化,如果原來存在被連接配接的class對象,則傳回該對象但是依然不會初始化。單參數時,預設initialize是為true的。
loadclass也是多态loadclass(string name)單參數時, resolve=false。如果該類已經被該類裝載器所裝載,那麼,傳回這個已經被裝載的類型的class的執行個體,否則,就用這個自定義的類裝載器來裝載這個class,這時不知道是否被連接配接。絕對不會被初始化!這時唯一可以保證的是,這個類被裝載了。但是不知道這個類是不是被連接配接和初始化了。
loadclass(string name, boolean resolve)resolve=true時,則保證已經裝載,而且已經連接配接了。 resolve=falses時,則僅僅是去裝載這個類,不關心是否連接配接了,是以此時可能被連接配接了,也可能沒有被連接配接。下面通過測試來驗證以上說的内容,代碼如下:
test類:
<span style="font-family:fangsong_gb2312;font-size:18px;"><span style="font-family:fangsong_gb2312;">public class test {
}
public test(){
system.out.println("無參數的構造函數");
public test(string str){
system.out.println("帶參數的構造函數");
測試一:用戶端調用代碼
}
輸出結果為:靜态初始化
說明:class.forname時類執行了裝載、連接配接、初始化三個步驟。
測試二:用戶端代碼改為
class c=class.forname("com.zjj.classtest.test", false, cl);
輸出結果為:initialize=true時輸出,靜态初始化。為false時沒有輸出
說明:為true時類執行了裝載、連接配接、初始化三個步驟。為false時沒有初始化,為知是不是連接配接。
測試三:用戶端代碼改為
c.newinstance();
靜态初始化
非靜态初始化
說明:為了保證jvm中不存在之前加載過的類,特地清理了jvm記憶體。但是輸出結果不變,說明為false時執行了裝載和連結,否則newinstance是無法執行的(前面說過了newinstance的執行條件)。但是資料說可能還存在不連接配接的情況!!有待考證。
測試四:用戶端代碼改為
class c=class.forname("com.zjj.classtest.test");
class c=class.forname("com.zjj.classtest.test", true, cl);
}
說明:如果原來存在加載過的類,那麼第二次執行加載請求時傳回存在的。因為,靜态初始化隻執行了一次。
測試五:用戶端代碼改為
<span style="font-family:fangsong_gb2312;font-size:18px;"><span style="font-family:fangsong_gb2312;">public static void main(string[] arg) throws classnotfoundexception, instantiationexception, illegalaccessexception{
//從目前線程取得正在運作的加載器
class c2=cl.loadclass("com.zjj.classtest.test").getclass(); //得到類的class對象
c2.newinstance(); //執行個體化對象
輸出結果:報錯
說明:此時loadclass方法加載到記憶體中的類是未連接配接的,當然不會初始化。是以也就沒有“靜态初始化”的輸出。
測試六:不知道為什麼沒有發現代碼中的classloader存在兩個參數的loadclass方法。
總結:至此方法對比結束,這篇部落客要是更細緻的了解了jvm加載類的過程和不同方式之間的差別。其實際上隻是封裝的程度不一樣,也就是方法的粒度的差别。當然,有一點内容還沒有通過自己的測試得到驗證,可能是我的方法不對或者是資料有問題。權且記下這個問題!