1. 程式人生 > >Java 反射基礎(下)

Java 反射基礎(下)

在上一篇部落格《 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; } }

訪問私有方法

以訪問 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) 方法,是獲取私有方法的訪問許可權,如果不加會報非法訪問異常,因為當前方法訪問許可權是“private”的:

java.lang.IllegalAccessException: 
Class MainClass can not access a member of
class obj.TestClass with modifiers "private"

正常執行後,列印如下,呼叫私有方法成功:

Java Reflect 666

修改私有變數

以修改 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

修改私有常量

真的能修改嗎?

私有常量是指使用 private 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
}

但是,並不是所有常量都會優化。經測試對於 intlongboolean 以及 String 這些基本型別 JVM 會優化,而對於 IntegerLongBoolean 這種包裝型別,或者其他諸如 DateObject 型別則不會被優化。

總結來說:對於基本型別的靜態常量,JVM 在編譯階段會把引用此常量的程式碼替換成具體的常量值

這麼說來,在實際開發中,如果我們想修改某個類的常量值,恰好那個常量是基本型別的,豈不是無能為力了?反正我個人認為除非修改原始碼,否則真沒辦法!

無能為力是指:我們在程式執行時刻依然可以使用反射修改常量的值(後面會程式碼驗證),但是 JVM 在編譯階段得到的 .class 檔案已經將常量優化為具體的值,在執行階段就直接使用具體的值了,所以即使修改了常量的值也已經毫無意義了,No Sense

下面我們驗證這一點,在測試類 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 檔案。為避免說程式碼是我自己手寫的,我決定不貼上程式碼,直接截圖:

TestClass.class 檔案

看到了吧,有圖有真相,getFinalValue() 方法直接 return "FINAL"!同時也說明了,程式執行時是根據編譯後的 .class 來執行的。再不信我我也沒辦法了哈 : )

順便提一下,如果您有時間,可以換幾個資料型別試試,正如上面說的,有些資料型別是不會優化的。您可以修改資料型別後,根據我的思路試試,看輸出覺得不靠譜就直接看 .classs 檔案,一眼就能看出來哪些資料型別優化了 ,哪些沒有優化。下面說下一個知識點。

想辦法也要修改!

不能修改,這您能忍?彆著急,不知您發現沒,剛才的常量都是在宣告時就直接賦值了。您可能會疑惑,常量不都是在宣告時賦值嗎?不賦值不報錯?當然不是啦,事實上,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 檔案的貼圖,圖中有標註。

TestClass.class 檔案

解釋一下:我們將賦值放在建構函式中,建構函式是我們執行時 new 物件才會呼叫的,所以就不會像之前直接為常量賦值那樣,在編譯階段將 getFinalValue() 方法優化為返回常量值,而是指向 FINAL_VALUE ,這樣我們在執行階段通過反射修改敞亮的值就有意義啦。但是,看得出來,程式還是有優化的,將建構函式中的賦值語句優化了。再想想那句”程式執行時是根據編譯後的 .class 來執行的“,相信您一定明白為什麼這麼輸出了!

請您務必將上面捋清楚了再往下看。接下來再說一種改法,不使用建構函式,也可以成功修改常量的值,但原理上都一樣。去掉建構函式,將宣告常量的語句改為使用三目表示式賦值:

private final String FINAL_VALUE
        = null == null ? "FINAL" : null;

其實,上述程式碼等價於直接為 FINAL_VALUE 賦值 “FINAL”,但是他就是可以!至於為什麼,您這麼想:null == null ? "FINAL" : null 是在執行時刻計算的,在編譯時刻不會計算,也就不會被優化,所以你懂得。

總結來說,不管使用建構函式還是三目表示式,根本上都是避免在編譯時刻被優化,這樣我們通過反射修改常量之後才有意義!好了,這一小部分到此結束!

最後的強調:必須提醒您的是,無論直接為常量賦值通過建構函式為常量賦值 還是 使用三目運算子,實際上我們都能通過反射成功修改常量的值。而我在上面說的修改”成功”與否是指:我們在程式執行階段通過反射肯定能修改常量值,但是實際執行優化後的 .class 檔案時,修改的後值真的起到作用了嗎?換句話說,就是編譯時是否將常量替換為具體的值了?如果替換了,再怎麼修改常量的值都不會影響最終的結果了,不是嗎?。其實,您可以直接這麼想:反射肯定能修改常量的值,但修改後的值是否有意義?

到底能不能改?

到底能不能改?也就是說反射修改後到底有沒有意義?如果您上面看明白了,答案就簡單了。俗話說“一千句話不如一張圖”,下面允許我用不太規範的流程圖直接表達答案哈。注:圖中”沒法修改”可以理解為”能修改值但沒有意義”;”可以修改”是指”能修改值且有意義”。

判斷能不能改

總結

好的,本次記錄就到這兒了,突然不知不覺發現寫了好多,這就是我習慣的延伸式學習,喜歡刨根問底 ==|| 我想這篇部落格如果您認真的看完,肯定會有收穫的!最後,因為內容較多,知識點較多,如果文中有任何錯誤或欠妥的地方,還望您指正,謝謝!

掃描下方二維碼,關注我的公眾號,及時獲取最新文章推送!