本博文主要記錄我學習 Java 反射(reflect)的一點心得,在瞭解反射之前,你應該先了解 Java 中的 Class 類,如果你不是很瞭解,可以先簡單瞭解下。
一、Java 反射機制
參考了許多博文,總結了以下個人觀點,若有不妥還望指正:
Java 反射機制在程式執行時,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性。這種 動態的獲取資訊 以及 動態呼叫物件的方法 的功能稱為 java 的反射機制。
反射機制很重要的一點就是“執行時”,其使得我們可以在程式執行時載入、探索以及使用編譯期間完全未知的
.class
檔案。換句話說,Java 程式可以載入一個執行時才得知名稱的.class
檔案,然後獲悉其完整構造,並生成其物件實體、或對其 fields(變數)設值、或呼叫其 methods(方法)。
不知道上面的理論你能否明白,反正剛接觸反射時我一臉懵比,後來寫了幾個例子之後:哦~~原來是這個意思!
若暫時不明白理論沒關係,先往下看例子,之後再回來看相信你就能明白了。
二、使用反射獲取類的資訊
為使得測試結果更加明顯,我首先定義了一個 FatherClass
類(預設繼承自 Object
類),然後定義一個繼承自 FatherClass
類的 SonClass
類,如下所示。可以看到測試類中變數以及方法的訪問許可權不是很規範,是為了更明顯得檢視測試結果而故意設定的,實際專案中不提倡這麼寫。
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. 獲取類的所有變數資訊
/**
* 通過反射獲取類的所有變數
*/
private static void printFields(){
//1.獲取並輸出類的名稱
Class mClass = SonClass.class;
System.out.println("類的名稱:" + mClass.getName());
//2.1 獲取所有 public 訪問許可權的變數
// 包括本類宣告的和從父類繼承的
Field[] fields = mClass.getFields();
//2.2 獲取所有本類宣告的變數(不問訪問許可權)
//Field[] fields = mClass.getDeclaredFields();
//3. 遍歷變數並輸出變數資訊
for (Field field :
fields) {
//獲取訪問許可權並輸出
int modifiers = field.getModifiers();
System.out.print(Modifier.toString(modifiers) + " ");
//輸出變數的型別及變數名
System.out.println(field.getType().getName()
+ " " + field.getName());
}
}複製程式碼
以上程式碼註釋很詳細,就不再解釋了。需要注意的是註釋中 2.1 的 getFields()
與 2.2的 getDeclaredFields()
之間的區別,下面分別看一下兩種情況下的輸出。看之前強調一下:SonClass
extends FatherClass
extends Object
:
呼叫
getFields()
方法,輸出SonClass
類以及其所繼承的父類( 包括FatherClass
和Object
) 的public
方法。注:Object
類中沒有成員變數,所以沒有輸出。類的名稱:obj.SonClass public java.lang.String mSonBirthday public java.lang.String mFatherName public int mFatherAge複製程式碼
呼叫
getDeclaredFields()
, 輸出SonClass
類的所有成員變數,不問訪問許可權。類的名稱:obj.SonClass private java.lang.String mSonName protected int mSonAge public java.lang.String mSonBirthday複製程式碼
2. 獲取類的所有方法資訊
/**
* 通過反射獲取類的所有方法
*/
private static void printMethods(){
//1.獲取並輸出類的名稱
Class mClass = SonClass.class;
System.out.println("類的名稱:" + mClass.getName());
//2.1 獲取所有 public 訪問許可權的方法
//包括自己宣告和從父類繼承的
Method[] mMethods = mClass.getMethods();
//2.2 獲取所有本類的的方法(不問訪問許可權)
//Method[] mMethods = mClass.getDeclaredMethods();
//3.遍歷所有方法
for (Method method :
mMethods) {
//獲取並輸出方法的訪問許可權(Modifiers:修飾符)
int modifiers = method.getModifiers();
System.out.print(Modifier.toString(modifiers) + " ");
//獲取並輸出方法的返回值型別
Class returnType = method.getReturnType();
System.out.print(returnType.getName() + " "
+ method.getName() + "( ");
//獲取並輸出方法的所有引數
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 的區別,下面看一下列印輸出:
呼叫
getMethods()
方法
獲取SonClass
類所有public
訪問許可權的方法,包括從父類繼承的。列印資訊中,printSonMsg()
方法來自SonClass
類,printFatherMsg()
來自FatherClass
類,其餘方法來自 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( )複製程式碼
呼叫
getDeclaredMethods()
方法列印資訊中,輸出的都是
SonClass
類的方法,不問訪問許可權。類的名稱: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( )複製程式碼
三、訪問或操作類的私有變數和方法
在上面,我們成功獲取了類的變數和方法資訊,驗證了在執行時 動態的獲取資訊 的觀點。那麼,僅僅是獲取資訊嗎?我們接著往後看。
都知道,物件是無法訪問或操作類的私有變數和方法的,但是,通過反射,我們就可以做到。沒錯,反射可以做到!下面,讓我們一起探討如何利用反射訪問 類物件的私有方法 以及修改 私有變數或常量。
老規矩,先上測試類。
注:
- 請注意看測試類中變數和方法的修飾符(訪問許可權);
- 測試類僅供測試,不提倡實際開發時這麼寫 : )
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 訪問私有方法
以訪問 TestClass
類中的私有方法 privateMethod(...)
為例,方法加引數是為了考慮最全的情況,很貼心有木有?先貼程式碼,看註釋,最後我會重點解釋部分程式碼。
/**
* 訪問物件的私有方法
* 為簡潔程式碼,在方法上丟擲總的異常,實際開發別這樣
*/
private static void getPrivateMethod() throws Exception{
//1. 獲取 Class 類例項
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();
//2. 獲取私有方法
//第一個引數為要獲取的私有方法的名稱
//第二個為要獲取方法的引數的型別,引數為 Class...,沒有引數就是null
//方法引數也可這麼寫 :new Class[]{String.class , int.class}
Method privateMethod =
mClass.getDeclaredMethod("privateMethod", String.class, int.class);
//3. 開始操作方法
if (privateMethod != null) {
//獲取私有方法的訪問權
//只是獲取訪問權,並不是修改實際許可權
privateMethod.setAccessible(true);
//使用 invoke 反射呼叫私有方法
//privateMethod 是獲取到的私有方法
//testClass 要操作的物件
//後面兩個引數傳實參
privateMethod.invoke(testClass, "Java Reflect ", 666);
}
}複製程式碼
需要注意的是,第3步中的 setAccessible(true)
方法,是獲取私有方法的訪問許可權,如果不加會報異常 IllegalAccessException,因為當前方法訪問許可權是“private”的,如下:
java.lang.IllegalAccessException: Class MainClass can not access a member of class obj.TestClass with modifiers "private"複製程式碼
正常執行後,列印如下,呼叫私有方法成功:
Java Reflect 666複製程式碼
3.2 修改私有變數
以修改 TestClass
類中的私有變數 MSG
為例,其初始值為 "Original" ,我們要修改為 "Modified"。老規矩,先上程式碼看註釋。
/**
* 修改物件私有變數的值
* 為簡潔程式碼,在方法上丟擲總的異常
*/
private static void modifyPrivateFiled() throws Exception {
//1. 獲取 Class 類例項
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();
//2. 獲取私有變數
Field privateField = mClass.getDeclaredField("MSG");
//3. 操作私有變數
if (privateField != null) {
//獲取私有變數的訪問權
privateField.setAccessible(true);
//修改私有變數,並輸出以測試
System.out.println("Before Modify:MSG = " + testClass.getMsg());
//呼叫 set(object , value) 修改變數的值
//privateField 是獲取到的私有變數
//testClass 要操作的物件
//"Modified" 為要修改成的值
privateField.set(testClass, "Modified");
System.out.println("After Modify:MSG = " + testClass.getMsg());
}
}複製程式碼
此處程式碼和訪問私有方法的邏輯差不多,就不再贅述,從輸出資訊看出 修改私有變數 成功:
Before Modify:MSG = Original
After Modify:MSG = Modified複製程式碼
3.3 修改私有常量
在 3.2 中,我們介紹瞭如何修改私有 變數,現在來說說如何修改私有 常量,
01. 真的能修改嗎?
常量是指使用 final
修飾符修飾的成員屬性,與變數的區別就在於有無 final
關鍵字修飾。在說之前,先補充一個知識點。
Java 虛擬機器(JVM)在編譯 .java
檔案得到 .class
檔案時,會優化我們的程式碼以提升效率。其中一個優化就是:JVM 在編譯階段會把引用常量的程式碼替換成具體的常量值,如下所示(部分程式碼)。
編譯前的 .java
檔案:
//注意是 String 型別的值
private final String FINAL_VALUE = "hello";
if(FINAL_VALUE.equals("world")){
//do something
}複製程式碼
編譯後得到的 .class
檔案(當然,編譯後是沒有註釋的):
private final String FINAL_VALUE = "hello";
//替換為"hello"
if("hello".equals("world")){
//do something
}複製程式碼
但是,並不是所有常量都會優化。經測試對於 int
、long
、boolean
以及 String
這些基本型別 JVM 會優化,而對於 Integer
、Long
、Boolean
這種包裝型別,或者其他諸如 Date
、Object
型別則不會被優化。
總結來說:對於基本型別的靜態常量,JVM 在編譯階段會把引用此常量的程式碼替換成具體的常量值。
這麼說來,在實際開發中,如果我們想修改某個類的常量值,恰好那個常量是基本型別的,豈不是無能為力了?反正我個人認為除非修改原始碼,否則真沒辦法!
這裡所謂的無能為力是指:我們在程式執行時刻依然可以使用反射修改常量的值(後面會程式碼驗證),但是 JVM 在編譯階段得到的 .class 檔案已經將常量優化為具體的值,在執行階段就直接使用具體的值了,所以即使修改了常量的值也已經毫無意義了。
下面我們驗證這一點,在測試類 TestClass
類中新增如下程式碼:
//String 會被 JVM 優化
private final String FINAL_VALUE = "FINAL";
public String getFinalValue(){
//劇透,會被優化為: return "FINAL" ,拭目以待吧
return FINAL_VALUE;
}複製程式碼
接下來,是修改常量的值,先上程式碼,請仔細看註釋:
/**
* 修改物件私有常量的值
* 為簡潔程式碼,在方法上丟擲總的異常,實際開發別這樣
*/
private static void modifyFinalFiled() throws Exception {
//1. 獲取 Class 類例項
TestClass testClass = new TestClass();
Class mClass = testClass.getClass();
//2. 獲取私有常量
Field finalField = mClass.getDeclaredField("FINAL_VALUE");
//3. 修改常量的值
if (finalField != null) {
//獲取私有常量的訪問權
finalField.setAccessible(true);
//呼叫 finalField 的 getter 方法
//輸出 FINAL_VALUE 修改前的值
System.out.println("Before Modify:FINAL_VALUE = "
+ finalField.get(testClass));
//修改私有常量
finalField.set(testClass, "Modified");
//呼叫 finalField 的 getter 方法
//輸出 FINAL_VALUE 修改後的值
System.out.println("After Modify:FINAL_VALUE = "
+ finalField.get(testClass));
//使用物件呼叫類的 getter 方法
//獲取值並輸出
System.out.println("Actually :FINAL_VALUE = "
+ testClass.getFinalValue());
}
}複製程式碼
上面的程式碼不解釋了,註釋巨詳細有木有!特別注意一下第3步的註釋,然後來看看輸出,已經迫不及待了,擦亮雙眼:
Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = FINAL複製程式碼
結果出來了:
第一句列印修改前 FINAL_VALUE
的值,沒有異議;
第二句列印修改後常量的值,說明FINAL_VALUE
確實通過反射修改了;
第三句列印通過 getFinalValue()
方法獲取的 FINAL_VALUE
的值,但還是初始值,導致修改無效!
這結果你覺得可信嗎?什麼,你還不信?問我怎麼知道 JVM 編譯後會優化程式碼?那要不這樣吧,一起來看看 TestClass.java
檔案編譯後得到的 TestClass.class
檔案。為避免說程式碼是我自己手寫的,我決定不貼上程式碼,直接截圖:
看到了吧,有圖有真相,getFinalValue()
方法直接 return "FINAL"
!同時也說明了,程式執行時是根據編譯後的 .class 來執行的。
順便提一下,如果你有時間,可以換幾個資料型別試試,正如上面說的,有些資料型別是不會優化的。你可以修改資料型別後,根據我的思路試試,看輸出覺得不靠譜就直接看 .classs
檔案,一眼就能看出來哪些資料型別優化了 ,哪些沒有優化。下面說下一個知識點。
02. 想辦法也要修改!
不能修改,這你能忍?彆著急,不知你發現沒,剛才的常量都是在宣告時就直接賦值了。你可能會疑惑,常量不都是在宣告時賦值嗎?不賦值不報錯?當然不是啦。
方法一
事實上,Java 允許我們宣告常量時不賦值,但必須在建構函式中賦值。你可能會問我為什麼要說這個,這就解釋:
我們修改一下 TestClass
類,在宣告常量時不賦值,然後新增建構函式併為其賦值,大概看一下修改後的程式碼(部分程式碼 ):
public class TestClass {
//......
private final String FINAL_VALUE;
//建構函式內為常量賦值
public TestClass(){
this.FINAL_VALUE = "FINAL";
}
//......
}複製程式碼
現在,我們再呼叫上面貼出的修改常量的方法,發現輸出是這樣的:
Before Modify:FINAL_VALUE = FINAL
After Modify:FINAL_VALUE = Modified
Actually :FINAL_VALUE = Modified複製程式碼
納尼,最後一句輸出修改後的值了?對,修改成功了!想知道為啥,還得看編譯後的 TestClass.class
檔案的貼圖,圖中有標註。
解釋一下:我們將賦值放在建構函式中,建構函式是我們執行時 new 物件才會呼叫的,所以就不會像之前直接為常量賦值那樣,在編譯階段將 getFinalValue()
方法優化為返回常量值,而是指向 FINAL_VALUE
,這樣我們在執行階段通過反射修改敞亮的值就有意義啦。但是,看得出來,程式還是有優化的,將建構函式中的賦值語句優化了。再想想那句 程式執行時是根據編譯後的 .class 來執行的 ,相信你一定明白為什麼這麼輸出了!
方法二
請你務必將上面捋清楚了再往下看。接下來再說一種改法,不使用建構函式,也可以成功修改常量的值,但原理上都一樣。去掉建構函式,將宣告常量的語句改為使用三目表示式賦值:
private final String FINAL_VALUE
= null == null ? "FINAL" : null;複製程式碼
其實,上述程式碼等價於直接為 FINAL_VALUE
賦值 "FINAL",但是他就是可以!至於為什麼,你這麼想:null == null ? "FINAL" : null
是在執行時刻計算的,在編譯時刻不會計算,也就不會被優化,所以你懂得。
總結來說,不管使用建構函式還是三目表示式,根本上都是避免在編譯時刻被優化,這樣我們通過反射修改常量之後才有意義!好了,這一小部分到此結束!
最後的強調:
必須提醒你的是,無論直接為常量賦值 、 通過建構函式為常量賦值 還是 使用三目運算子,實際上我們都能通過反射成功修改常量的值。而我在上面說的修改"成功"與否是指:我們在程式執行階段通過反射肯定能修改常量值,但是實際執行優化後的 .class 檔案時,修改的後值真的起到作用了嗎?換句話說,就是編譯時是否將常量替換為具體的值了?如果替換了,再怎麼修改常量的值都不會影響最終的結果了,不是嗎?。
其實,你可以直接這麼想:反射肯定能修改常量的值,但修改後的值是否有意義?
03. 到底能不能改?
到底能不能改?也就是說反射修改後到底有沒有意義?
如果你上面看明白了,答案就簡單了。俗話說“一千句話不如一張圖”,下面允許我用不太規範的流程圖直接表達答案哈。
注:圖中"沒法修改"可以理解為"能修改值但沒有意義";"可以修改"是指"能修改值且有意義"。
四、總結
好了,本次記錄就到這兒了,突然不知不覺發現寫了好多,感謝耐心聽我叨逼完。我想這篇部落格如果你認真的看完,肯定會有收穫的!最後,因為內容較多,知識點較多,如果文中有任何錯誤或欠妥的地方,還望指正。歡迎留言交流!
掃描下方二維碼,關注我的公眾號,及時獲取最新文章推送!