Java 反射由淺入深 | 進階必備

本博文主要記錄我學習 Java 反射(reflect)的一點心得,在瞭解反射以前,你應該先了解 Java 中的 Class 類,若是你不是很瞭解,能夠先簡單瞭解下。java

1、Java 反射機制

參考了許多博文,總結了如下我的觀點,如有不妥還望指正:bash

Java 反射機制在程序運行時,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意一個方法和屬性。這種 動態的獲取信息 以及 動態調用對象的方法 的功能稱爲 java 的反射機制函數

反射機制很重要的一點就是「運行時」,其使得咱們能夠在程序運行時加載、探索以及使用編譯期間徹底未知的 .class 文件。換句話說,Java 程序能夠加載一個運行時才得知名稱的 .class 文件,而後獲悉其完整構造,並生成其對象實體、或對其 fields(變量)設值、或調用其 methods(方法)。學習

不知道上面的理論你可否明白,反正剛接觸反射時我一臉懵比,後來寫了幾個例子以後:哦~~原來是這個意思!測試

若暫時不明白理論不要緊,先往下看例子,以後再回來看相信你就能明白了。優化

2、使用反射獲取類的信息

爲使得測試結果更加明顯,我首先定義了一個 FatherClass 類(默認繼承自 Object 類),而後定義一個繼承自 FatherClass 類的 SonClass 類,以下所示。能夠看到測試類中變量以及方法的訪問權限不是很規範,是爲了更明顯得查看測試結果而故意設置的,實際項目中不提倡這麼寫。ui

FatherClass.javathis

public class FatherClass {
    public String mFatherName;
    public int mFatherAge;

    public void printFatherMsg(){}
}複製代碼

SonClass.javaspa

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 Objectcode

  • 調用 getFields() 方法,輸出 SonClass 類以及其所繼承的父類( 包括 FatherClassObject ) 的 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(  )複製代碼

3、訪問或操做類的私有變量和方法

在上面,咱們成功獲取了類的變量和方法信息,驗證了在運行時 動態的獲取信息 的觀點。那麼,僅僅是獲取信息嗎?咱們接着日後看。

都知道,對象是沒法訪問或操做類的私有變量和方法的,可是,經過反射,咱們就能夠作到。沒錯,反射能夠作到!下面,讓咱們一塊兒探討如何利用反射訪問 類對象的私有方法 以及修改 私有變量或常量

老規矩,先上測試類。

注:

  1. 請注意看測試類中變量和方法的修飾符(訪問權限);
  2. 測試類僅供測試,不提倡實際開發時這麼寫 : )

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
}複製代碼

可是,並非全部常量都會優化。經測試對於 intlongboolean 以及 String 這些基本類型 JVM 會優化,而對於 IntegerLongBoolean 這種包裝類型,或者其餘諸如 DateObject 類型則不會被優化。

總結來講:對於基本類型的靜態常量,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 文件。爲避免說代碼是我本身手寫的,我決定不粘貼代碼,直接截圖:

TestClass.class 文件
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 文件的貼圖,圖中有標註。

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 文件時,修改的後值真的起到做用了嗎?換句話說,就是編譯時是否將常量替換爲具體的值了?若是替換了,再怎麼修改常量的值都不會影響最終的結果了,不是嗎?

其實,你能夠直接這麼想:反射確定能修改常量的值,但修改後的值是否有意義

03. 到底能不能改?

到底能不能改?也就是說反射修改後到底有沒有意義?

若是你上面看明白了,答案就簡單了。俗話說「一千句話不如一張圖」,下面容許我用不太規範的流程圖直接表達答案哈。

注:圖中"無法修改"能夠理解爲"能修改值但沒有意義";"能夠修改"是指"能修改值且有意義"。

判斷能不能改
判斷能不能改

4、總結

好了,本次記錄就到這兒了,忽然不知不覺發現寫了好多,感謝耐心聽我叨逼完。我想這篇博客若是你認真的看完,確定會有收穫的!最後,由於內容較多,知識點較多,若是文中有任何錯誤或欠妥的地方,還望指正。歡迎留言交流!


掃描下方二維碼,關注個人公衆號,及時獲取最新文章推送!

Android進階之旅
Android進階之旅
相關文章
相關標籤/搜索