進(jìn)階必備 - Java 反射由淺入深
本博文主要記錄我學(xué)習(xí) Java 反射(reflect)的一點(diǎn)心得,在了解反射之前,你應(yīng)該先了解 Java 中的 Class 類,如果你不是很了解,可以先簡(jiǎn)單了解下。
一、Java 反射機(jī)制參考了許多博文,總結(jié)了以下個(gè)人觀點(diǎn),若有不妥還望指正:
Java 反射機(jī)制在程序 運(yùn)行時(shí) ,對(duì)于任意一個(gè)類,都能夠知道這個(gè)類的所有屬性和方法;對(duì)于任意一個(gè)對(duì)象,都能夠調(diào)用它的任意一個(gè)方法和屬性。這種 動(dòng)態(tài)的獲取信息 以及 動(dòng)態(tài)調(diào)用對(duì)象的方法 的功能稱為 java 的反射機(jī)制 。
反射機(jī)制很重要的一點(diǎn)就是“運(yùn)行時(shí)”,其使得我們可以在程序運(yùn)行時(shí)加載、探索以及使用編譯期間完全未知的 .class 文件。換句話說(shuō),Java 程序可以加載一個(gè)運(yùn)行時(shí)才得知名稱的 .class 文件,然后獲悉其完整構(gòu)造,并生成其對(duì)象實(shí)體、或?qū)ζ?fields(變量)設(shè)值、或調(diào)用其 methods(方法)。
不知道上面的理論你能否明白,反正剛接觸反射時(shí)我一臉懵比,后來(lái)寫(xiě)了幾個(gè)例子之后:哦~~原來(lái)是這個(gè)意思!
若暫時(shí)不明白理論沒(méi)關(guān)系,先往下看例子,之后再回來(lái)看相信你就能明白了。
二、使用反射獲取類的信息為使得測(cè)試結(jié)果更加明顯,我首先定義了一個(gè) FatherClass 類(默認(rèn)繼承自 Object 類),然后定義一個(gè)繼承自 FatherClass 類的 SonClass 類,如下所示。可以看到測(cè)試類中變量以及方法的訪問(wèn)權(quán)限不是很規(guī)范,是為了更明顯得查看測(cè)試結(jié)果而故意設(shè)置的,實(shí)際項(xiàng)目中不提倡這么寫(xiě)。
FatherClass.java
public class FatherClass { public String mFatherName; public int mFatherAge; public void printFatherMsg(){}}
SonClass.java
public class SonClass extends FatherClass{ private String mSonName; protected int mSonAge; public String mSonBirthday; public void printSonMsg(){System.out.println('Son Msg - name : '+ mSonName + '; age : ' + mSonAge); } private void setSonName(String name){mSonName = name; } private void setSonAge(int age){mSonAge = age; } private int getSonAge(){return mSonAge; } private String getSonName(){return mSonName; }} 1. 獲取類的所有變量信息
/** * 通過(guò)反射獲取類的所有變量 */private static void printFields(){ //1.獲取并輸出類的名稱 Class mClass = SonClass.class; System.out.println('類的名稱:' + mClass.getName()); //2.1 獲取所有 public 訪問(wèn)權(quán)限的變量 // 包括本類聲明的和從父類繼承的 Field[] fields = mClass.getFields(); //2.2 獲取所有本類聲明的變量(不問(wèn)訪問(wèn)權(quán)限) //Field[] fields = mClass.getDeclaredFields(); //3. 遍歷變量并輸出變量信息 for (Field field : fields) {//獲取訪問(wèn)權(quán)限并輸出int modifiers = field.getModifiers();System.out.print(Modifier.toString(modifiers) + ' ');//輸出變量的類型及變量名System.out.println(field.getType().getName() + ' ' + field.getName()); }}
以上代碼注釋很詳細(xì),就不再解釋了。需要注意的是注釋中 2.1 的 getFields() 與 2.2的 getDeclaredFields() 之間的區(qū)別,下面分別看一下兩種情況下的輸出。看之前強(qiáng)調(diào)一下:
SonClass extends FatherClass extends Object :
調(diào)用 getFields() 方法,輸出 SonClass 類以及其所繼承的父類( 包括 FatherClass 和 Object ) 的 public 方法。注: Object 類中沒(méi)有成員變量,所以沒(méi)有輸出。
類的名稱:obj.SonClass public java.lang.String mSonBirthday public java.lang.String mFatherName public int mFatherAge
調(diào)用 getDeclaredFields() , 輸出 SonClass 類的所有成員變量,不問(wèn)訪問(wèn)權(quán)限。
類的名稱:obj.SonClass private java.lang.String mSonName protected int mSonAge public java.lang.String mSonBirthday 2. 獲取類的所有方法信息
/** * 通過(guò)反射獲取類的所有方法 */private static void printMethods(){ //1.獲取并輸出類的名稱 Class mClass = SonClass.class; System.out.println('類的名稱:' + mClass.getName()); //2.1 獲取所有 public 訪問(wèn)權(quán)限的方法 //包括自己聲明和從父類繼承的 Method[] mMethods = mClass.getMethods(); //2.2 獲取所有本類的的方法(不問(wèn)訪問(wèn)權(quán)限) //Method[] mMethods = mClass.getDeclaredMethods(); //3.遍歷所有方法 for (Method method : mMethods) {//獲取并輸出方法的訪問(wèn)權(quán)限(Modifiers:修飾符)int modifiers = method.getModifiers();System.out.print(Modifier.toString(modifiers) + ' ');//獲取并輸出方法的返回值類型Class returnType = method.getReturnType();System.out.print(returnType.getName() + ' '+ method.getName() + '( ');//獲取并輸出方法的所有參數(shù)Parameter[] parameters = method.getParameters();for (Parameter parameter: parameters) { System.out.print(parameter.getType().getName() + ' ' + parameter.getName() + ',');}//獲取并輸出方法拋出的異常Class[] exceptionTypes = method.getExceptionTypes();if (exceptionTypes.length == 0){ System.out.println(' )');}else { for (Class c : exceptionTypes) {System.out.println(' ) throws '+ c.getName()); }} }}
同獲取變量信息一樣,需要注意注釋中 2.1 與 2.2 的區(qū)別,下面看一下打印輸出:
調(diào)用 getMethods() 方法
獲取 SonClass 類所有 public 訪問(wèn)權(quán)限的方法,包括從父類繼承的。打印信息中, printSonMsg() 方法來(lái)自 SonClass 類, printFatherMsg() 來(lái)自 FatherClass 類,其余方法來(lái)自 Object 類。
類的名稱:obj.SonClass public void printSonMsg( ) public void printFatherMsg( ) public final void wait( ) throws java.lang.InterruptedException public final void wait( long arg0,int arg1, ) throws java.lang.InterruptedException public final native void wait( long arg0, ) throws java.lang.InterruptedException public boolean equals( java.lang.Object arg0, ) public java.lang.String toString( ) public native int hashCode( ) public final native java.lang.Class getClass( ) public final native void notify( ) public final native void notifyAll( )
調(diào)用 getDeclaredMethods() 方法
打印信息中,輸出的都是 SonClass 類的方法,不問(wèn)訪問(wèn)權(quán)限。
類的名稱:obj.SonClass private int getSonAge( ) private void setSonAge( int arg0, ) public void printSonMsg( ) private void setSonName( java.lang.String arg0, ) private java.lang.String getSonName( ) 三、訪問(wèn)或操作類的私有變量和方法
在上面,我們成功獲取了類的變量和方法信息,驗(yàn)證了在運(yùn)行時(shí) 動(dòng)態(tài)的獲取信息 的觀點(diǎn)。那么,僅僅是獲取信息嗎?我們接著往后看。
都知道,對(duì)象是無(wú)法訪問(wèn)或操作類的私有變量和方法的,但是,通過(guò)反射,我們就可以做到。沒(méi)錯(cuò),反射可以做到!下面,讓我們一起探討如何利用反射訪問(wèn) 類對(duì)象的私有方法 以及修改 私有變量或常量 。
老規(guī)矩,先上測(cè)試類。
注:
請(qǐng)注意看測(cè)試類中變量和方法的修飾符(訪問(wèn)權(quán)限); 測(cè)試類僅供測(cè)試,不提倡實(shí)際開(kāi)發(fā)時(shí)這么寫(xiě) : )TestClass.java
public class TestClass { private String MSG = 'Original'; private void privateMethod(String head , int tail){System.out.print(head + tail); } public String getMsg(){return MSG; }} 3.1 訪問(wèn)私有方法
以訪問(wèn) TestClass 類中的私有方法 privateMethod(...) 為例,方法加參數(shù)是為了考慮最全的情況,很貼心有木有?先貼代碼,看注釋,最后我會(huì)重點(diǎn)解釋部分代碼。
/** * 訪問(wèn)對(duì)象的私有方法 * 為簡(jiǎn)潔代碼,在方法上拋出總的異常,實(shí)際開(kāi)發(fā)別這樣 */private static void getPrivateMethod() throws Exception{ //1. 獲取 Class 類實(shí)例 TestClass testClass = new TestClass(); Class mClass = testClass.getClass(); //2. 獲取私有方法 //第一個(gè)參數(shù)為要獲取的私有方法的名稱 //第二個(gè)為要獲取方法的參數(shù)的類型,參數(shù)為 Class...,沒(méi)有參數(shù)就是null //方法參數(shù)也可這么寫(xiě) :new Class[]{String.class , int.class} Method privateMethod = mClass.getDeclaredMethod('privateMethod', String.class, int.class); //3. 開(kāi)始操作方法 if (privateMethod != null) {//獲取私有方法的訪問(wèn)權(quán)//只是獲取訪問(wèn)權(quán),并不是修改實(shí)際權(quán)限privateMethod.setAccessible(true);//使用 invoke 反射調(diào)用私有方法//privateMethod 是獲取到的私有方法//testClass 要操作的對(duì)象//后面兩個(gè)參數(shù)傳實(shí)參privateMethod.invoke(testClass, 'Java Reflect ', 666); }}
需要注意的是,第3步中的 setAccessible(true) 方法,是獲取私有方法的訪問(wèn)權(quán)限,如果不加會(huì)報(bào)異常 IllegalAccessException ,因?yàn)楫?dāng)前方法訪問(wèn)權(quán)限是“private”的,如下:
java.lang.IllegalAccessException: Class MainClass can not access a member of class obj.TestClass with modifiers 'private'
正常運(yùn)行后,打印如下, 調(diào)用私有方法 成功:
Java Reflect 666 3.2 修改私有變量
以修改 TestClass 類中的私有變量 MSG 為例,其初始值為 'Original' ,我們要修改為 'Modified'。老規(guī)矩,先上代碼看注釋。
/** * 修改對(duì)象私有變量的值 * 為簡(jiǎn)潔代碼,在方法上拋出總的異常 */private static void modifyPrivateFiled() throws Exception { //1. 獲取 Class 類實(shí)例 TestClass testClass = new TestClass(); Class mClass = testClass.getClass(); //2. 獲取私有變量 Field privateField = mClass.getDeclaredField('MSG'); //3. 操作私有變量 if (privateField != null) {//獲取私有變量的訪問(wèn)權(quán)privateField.setAccessible(true);//修改私有變量,并輸出以測(cè)試System.out.println('Before Modify:MSG = ' + testClass.getMsg());//調(diào)用 set(object , value) 修改變量的值//privateField 是獲取到的私有變量//testClass 要操作的對(duì)象//'Modified' 為要修改成的值privateField.set(testClass, 'Modified');System.out.println('After Modify:MSG = ' + testClass.getMsg()); }}
此處代碼和訪問(wèn)私有方法的邏輯差不多,就不再贅述,從輸出信息看出 修改私有變量 成功:
Before Modify:MSG = OriginalAfter Modify:MSG = Modified 3.3 修改私有常量
在 3.2 中,我們介紹了如何修改私有 變量 ,現(xiàn)在來(lái)說(shuō)說(shuō)如何修改私有 常量 ,
01. 真的能修改嗎?常量是指使用 final 修飾符修飾的成員屬性,與變量的區(qū)別就在于有無(wú) final 關(guān)鍵字修飾。在說(shuō)之前,先補(bǔ)充一個(gè)知識(shí)點(diǎn)。
Java 虛擬機(jī)(JVM)在編譯 .java 文件得到 .class 文件時(shí),會(huì)優(yōu)化我們的代碼以提升效率。其中一個(gè)優(yōu)化就是:JVM 在編譯階段會(huì)把引用常量的代碼替換成具體的常量值,如下所示(部分代碼)。
編譯前的 .java 文件:
//注意是 String 類型的值private final String FINAL_VALUE = 'hello';if(FINAL_VALUE.equals('world')){ //do something}
編譯后得到的 .class 文件(當(dāng)然,編譯后是沒(méi)有注釋的):
private final String FINAL_VALUE = 'hello';//替換為'hello'if('hello'.equals('world')){ //do something}
但是,并不是所有常量都會(huì)優(yōu)化。經(jīng)測(cè)試對(duì)于 int 、 long 、 boolean 以及 String 這些基本類型 JVM 會(huì)優(yōu)化,而對(duì)于 Integer 、 Long 、 Boolean 這種包裝類型,或者其他諸如 Date 、 Object 類型則不會(huì)被優(yōu)化。
總結(jié)來(lái)說(shuō): 對(duì)于基本類型的靜態(tài)常量,JVM 在編譯階段會(huì)把引用此常量的代碼替換成具體的常量值 。
這么說(shuō)來(lái),在實(shí)際開(kāi)發(fā)中,如果我們想修改某個(gè)類的常量值,恰好那個(gè)常量是基本類型的,豈不是無(wú)能為力了?反正我個(gè)人認(rèn)為除非修改源碼,否則真沒(méi)辦法!
這里所謂的無(wú)能為力是指: 我們?cè)诔绦蜻\(yùn)行時(shí)刻依然可以使用反射修改常量的值(后面會(huì)代碼驗(yàn)證),但是 JVM 在編譯階段得到的 .class 文件已經(jīng)將常量?jī)?yōu)化為具體的值,在運(yùn)行階段就直接使用具體的值了,所以即使修改了常量的值也已經(jīng)毫無(wú)意義了 。
下面我們驗(yàn)證這一點(diǎn),在測(cè)試類 TestClass 類中添加如下代碼:
//String 會(huì)被 JVM 優(yōu)化private final String FINAL_VALUE = 'FINAL';public String getFinalValue(){ //劇透,會(huì)被優(yōu)化為: return 'FINAL' ,拭目以待吧 return FINAL_VALUE;}
接下來(lái),是修改常量的值,先上代碼,請(qǐng)仔細(xì)看注釋:
/** * 修改對(duì)象私有常量的值 * 為簡(jiǎn)潔代碼,在方法上拋出總的異常,實(shí)際開(kāi)發(fā)別這樣 */private static void modifyFinalFiled() throws Exception { //1. 獲取 Class 類實(shí)例 TestClass testClass = new TestClass(); Class mClass = testClass.getClass(); //2. 獲取私有常量 Field finalField = mClass.getDeclaredField('FINAL_VALUE'); //3. 修改常量的值 if (finalField != null) {//獲取私有常量的訪問(wèn)權(quán)finalField.setAccessible(true);//調(diào)用 finalField 的 getter 方法//輸出 FINAL_VALUE 修改前的值System.out.println('Before Modify:FINAL_VALUE = '+ finalField.get(testClass));//修改私有常量finalField.set(testClass, 'Modified');//調(diào)用 finalField 的 getter 方法//輸出 FINAL_VALUE 修改后的值System.out.println('After Modify:FINAL_VALUE = '+ finalField.get(testClass));//使用對(duì)象調(diào)用類的 getter 方法//獲取值并輸出System.out.println('Actually :FINAL_VALUE = '+ testClass.getFinalValue()); }}
上面的代碼不解釋了,注釋巨詳細(xì)有木有!特別注意一下第3步的注釋,然后來(lái)看看輸出,已經(jīng)迫不及待了,擦亮雙眼:
Before Modify:FINAL_VALUE = FINALAfter Modify:FINAL_VALUE = ModifiedActually :FINAL_VALUE = FINAL
結(jié)果出來(lái)了:
第一句打印修改前 FINAL_VALUE 的值,沒(méi)有異議;
第二句打印修改后常量的值,說(shuō)明 FINAL_VALUE 確實(shí)通過(guò)反射修改了;
第三句打印通過(guò) getFinalValue() 方法獲取的 FINAL_VALUE 的值,但還是初始值,導(dǎo)致修改無(wú)效!
這結(jié)果你覺(jué)得可信嗎?什么,你還不信?問(wèn)我怎么知道 JVM 編譯后會(huì)優(yōu)化代碼?那要不這樣吧,一起來(lái)看看 TestClass.java 文件編譯后得到的 TestClass.class 文件。為避免說(shuō)代碼是我自己手寫(xiě)的,我決定不粘貼代碼,直接截圖:
TestClass.class 文件
看到了吧,有圖有真相, getFinalValue() 方法直接 return 'FINAL' !同時(shí)也說(shuō)明了, 程序運(yùn)行時(shí)是根據(jù)編譯后的 .class 來(lái)執(zhí)行的 。
順便提一下,如果你有時(shí)間,可以換幾個(gè)數(shù)據(jù)類型試試,正如上面說(shuō)的,有些數(shù)據(jù)類型是不會(huì)優(yōu)化的。你可以修改數(shù)據(jù)類型后,根據(jù)我的思路試試,看輸出覺(jué)得不靠譜就直接看 .classs 文件,一眼就能看出來(lái)哪些數(shù)據(jù)類型優(yōu)化了 ,哪些沒(méi)有優(yōu)化。下面說(shuō)下一個(gè)知識(shí)點(diǎn)。
02. 想辦法也要修改!不能修改,這你能忍?別著急,不知你發(fā)現(xiàn)沒(méi),剛才的常量都是在聲明時(shí)就直接賦值了。你可能會(huì)疑惑,常量不都是在聲明時(shí)賦值嗎?不賦值不報(bào)錯(cuò)?當(dāng)然不是啦。
方法一
事實(shí)上,Java 允許我們聲明常量時(shí)不賦值,但必須在構(gòu)造函數(shù)中賦值。你可能會(huì)問(wèn)我為什么要說(shuō)這個(gè),這就解釋:
我們修改一下 TestClass 類,在聲明常量時(shí)不賦值,然后添加構(gòu)造函數(shù)并為其賦值,大概看一下修改后的代碼(部分代碼 ):
public class TestClass { //...... private final String FINAL_VALUE; //構(gòu)造函數(shù)內(nèi)為常量賦值 public TestClass(){this.FINAL_VALUE = 'FINAL'; } //......}
現(xiàn)在,我們?cè)僬{(diào)用上面貼出的修改常量的方法,發(fā)現(xiàn)輸出是這樣的:
Before Modify:FINAL_VALUE = FINALAfter Modify:FINAL_VALUE = ModifiedActually :FINAL_VALUE = Modified
納尼,最后一句輸出修改后的值了?對(duì),修改成功了!想知道為啥,還得看編譯后的 TestClass.class 文件的貼圖,圖中有標(biāo)注。
TestClass.class 文件
解釋一下:我們將賦值放在構(gòu)造函數(shù)中,構(gòu)造函數(shù)是我們運(yùn)行時(shí) new 對(duì)象才會(huì)調(diào)用的,所以就不會(huì)像之前直接為常量賦值那樣,在編譯階段將 getFinalValue() 方法優(yōu)化為返回常量值,而是指向 FINAL_VALUE ,這樣我們?cè)谶\(yùn)行階段通過(guò)反射修改敞亮的值就有意義啦。但是,看得出來(lái),程序還是有優(yōu)化的,將構(gòu)造函數(shù)中的賦值語(yǔ)句優(yōu)化了。再想想那句 程序運(yùn)行時(shí)是根據(jù)編譯后的 .class 來(lái)執(zhí)行的 ,相信你一定明白為什么這么輸出了!
方法二
請(qǐng)你務(wù)必將上面捋清楚了再往下看。接下來(lái)再說(shuō)一種改法,不使用構(gòu)造函數(shù),也可以成功修改常量的值,但原理上都一樣。去掉構(gòu)造函數(shù),將聲明常量的語(yǔ)句改為使用三目表達(dá)式賦值:
private final String FINAL_VALUE= null == null ? 'FINAL' : null;
其實(shí),上述代碼等價(jià)于直接為 FINAL_VALUE 賦值 'FINAL',但是他就是可以!至于為什么,你這么想: null == null ? 'FINAL' : null 是在運(yùn)行時(shí)刻計(jì)算的,在編譯時(shí)刻不會(huì)計(jì)算,也就不會(huì)被優(yōu)化,所以你懂得。
總結(jié)來(lái)說(shuō),不管使用構(gòu)造函數(shù)還是三目表達(dá)式,根本上都是 避免在編譯時(shí)刻被優(yōu)化 ,這樣我們通過(guò)反射修改常量之后才有意義!好了,這一小部分到此結(jié)束!
最后的強(qiáng)調(diào):
必須提醒你的是,無(wú)論 直接為常量賦值 、 通過(guò)構(gòu)造函數(shù)為常量賦值 還是 使用三目運(yùn)算符 ,實(shí)際上我們都能通過(guò)反射成功修改常量的值。而我在上面說(shuō)的修改'成功'與否是指: 我們?cè)诔绦蜻\(yùn)行階段通過(guò)反射肯定能修改常量值,但是實(shí)際執(zhí)行優(yōu)化后的 .class 文件時(shí),修改的后值真的起到作用了嗎?換句話說(shuō),就是編譯時(shí)是否將常量替換為具體的值了?如果替換了,再怎么修改常量的值都不會(huì)影響最終的結(jié)果了,不是嗎? 。
其實(shí),你可以直接這么想: 反射肯定能修改常量的值,但修改后的值是否有意義 ?
03. 到底能不能改?到底能不能改?也就是說(shuō)反射修改后到底有沒(méi)有意義?
如果你上面看明白了,答案就簡(jiǎn)單了。俗話說(shuō)“一千句話不如一張圖”,下面允許我用不太規(guī)范的流程圖直接表達(dá)答案哈。
注:圖中'沒(méi)法修改'可以理解為'能修改值但沒(méi)有意義';'可以修改'是指'能修改值且有意義'。
判斷能不能改
四、總結(jié)好了,本次記錄就到這兒了,突然不知不覺(jué)發(fā)現(xiàn)寫(xiě)了好多,感謝耐心聽(tīng)我叨逼完。我想這篇博客如果你認(rèn)真的看完,肯定會(huì)有收獲的!最后,因?yàn)閮?nèi)容較多,知識(shí)點(diǎn)較多,如果文中有任何錯(cuò)誤或欠妥的地方,還望指正。歡迎留言交流!
掃描下方二維碼,關(guān)注我的公眾號(hào),及時(shí)獲取最新文章推送!
Android進(jìn)階之旅
相關(guān)文章:
1. moment轉(zhuǎn)化時(shí)間戳出現(xiàn)Invalid Date的問(wèn)題及解決2. python爬蟲(chóng)實(shí)戰(zhàn)之制作屬于自己的一個(gè)IP代理模塊3. WML的簡(jiǎn)單例子及編輯、測(cè)試方法第1/2頁(yè)4. HTML 絕對(duì)路徑與相對(duì)路徑概念詳細(xì)5. 如何在jsp界面中插入圖片6. ajax請(qǐng)求后臺(tái)得到j(luò)son數(shù)據(jù)后動(dòng)態(tài)生成樹(shù)形下拉框的方法7. Ajax返回值類型與用法實(shí)例分析8. .NET6打包部署到Windows Service的全過(guò)程9. 開(kāi)發(fā)效率翻倍的Web API使用技巧10. asp批量添加修改刪除操作示例代碼
