Java反射深入解析(一)---基礎篇

目錄

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

一、Java 反射機制

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

1. 獲取類的所有變量信息

輸出日誌:

輸出日誌:

2. 獲取類的所有方法信息

三、訪問或操作類的私有變量和方法

3.1 訪問私有方法

3.2 修改私有變量

3.2 修改私有常量

01. 真的能修改嗎?

02. 想辦法也要修改!

03. 到底能不能改?


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

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

一、Java 反射機制

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

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

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

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

FatherClass.java

/**
 * Java反射測試--父類
 */
public class FatherClass {
    public String mFatherName;

    public int mFatherAge;

    public void printFatherMsg(){

    }
}

SonClass.java
 

package com.java.basic.reflect;

/**
 * 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);
    }

    /**
     * getter setter設置
     * @param name
     */
    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. 獲取類的所有變量信息

/**
     * 通過反射獲取類的所有變量信息
     */
    @Test
    public void test1(){

        // 獲取並輸出類的名稱
        Class mClass = SonClass.class;
        System.out.println("子類的名稱: "+ mClass.getName());

        //2.1 獲取所有 public 訪問權限的變量
        // 包括本類聲明的和從父類繼承的
        Field [] fields = mClass.getFields();


        // 2.2 獲取所有本類聲明的變量(不問訪問權限)
        // Field[] fields = mClass.getDeclaredFields();

        // 遍歷輸出所有變量信息:public protected private
        for (Field field: fields){

            int modifers = field.getModifiers();
            System.out.print(Modifier.toString(modifers)+" ");

            // 輸出變量的類型及其變量名
            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 類中沒有成員變量,所以沒有輸出。

輸出日誌:

獲取類的所有變量信息: Begin
子類的名稱: com.java.basic.reflect.SonClass
public java.lang.String mSonBirthday
public java.lang.String mFatherName
public int mFatherAge
獲取類的所有變量信息: End

調用 getDeclaredFields() , 輸出 SonClass 類的所有成員變量,不問訪問權限。也就是說這裏可以輸出任何訪問權限的變量信息: public,private,protected。

/**
     * 通過反射獲取類的所有變量信息:不問訪問權限
     */
    @Test
    public void test2(){

        // 獲取並輸出類的名稱
        Class mClass = SonClass.class;
        System.out.println("子類的名稱: "+ mClass.getName());

        //2.1 獲取所有 public 訪問權限的變量
        // 包括本類聲明的和從父類繼承的
        // Field [] fields = mClass.getFields();


        // 2.2 獲取所有本類聲明的變量(不問訪問權限)
         Field[] fields = mClass.getDeclaredFields();

        // 遍歷輸出所有變量信息:public protected private
        for (Field field: fields){

            int modifers = field.getModifiers();
            System.out.print(Modifier.toString(modifers)+" ");

            // 輸出變量的類型及其變量名
            System.out.println(field.getType().getName()+" "+field.getName());
        }
    }

輸出日誌:

獲取類的所有變量信息: Begin
子類的名稱: com.java.basic.reflect.SonClass
private java.lang.String mSonName
protected int mSonAge
public java.lang.String mSonBirthday
獲取類的所有變量信息: End

2. 獲取類的所有方法信息

/**
     * 通過反射獲取類的所有方法信息信息:公共方法(public)
     */
    @Test
    public void test3(){

        // 獲取並輸出類的名稱
        Class mClass = SonClass.class;
        System.out.println("子類的名稱: \n"+ mClass.getName());

        //2.1 獲取所有 public 訪問權限的方法
        //包括自己聲明和從父類繼承的
        Method[] mMethods = mClass.getMethods();
        
        //2.2 獲取所有本類的的方法(不問訪問權限)
        //Method[] mMethods = mClass.getDeclaredMethods();

        // 遍歷輸出所有變量信息:public protected private
        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 [] exceptionsType = method.getExceptionTypes();
            if (exceptionsType.length==0){
                System.out.println(")");
                System.out.println();
            }else{
                for (Class c:exceptionsType){
                    System.out.print(") throws "+ c.getName());
                }
                System.out.println();
            }

        }
    }

同獲取變量信息一樣,需要注意註釋中 2.1 與 2.2 的區別,下面看一下打印輸出:

  • 調用 getMethods() 方法
    獲取 SonClass 類所有 public 訪問權限的方法,包括從父類繼承的。打印信息中,printSonMsg() 方法來自 SonClass 類, printFatherMsg() 來自 FatherClass 類,其餘方法來自 Object 類。

獲取類的所有變量信息: Begin
子類的名稱: 
com.java.basic.reflect.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()

獲取類的所有變量信息: End
  • 調用 getDeclaredMethods() 方法

打印信息中,輸出的都是 SonClass 類的方法,不問訪問權限。

/**
     * 通過反射獲取類的所有方法信息信息:公共方法(public)
     */
    @Test
    public void test4(){

        // 獲取並輸出類的名稱
        Class mClass = SonClass.class;
        System.out.println("子類的名稱: \n"+ mClass.getName());

        //2.1 獲取所有 public 訪問權限的方法
        //包括自己聲明和從父類繼承的
        // Method[] mMethods = mClass.getMethods();

        //2.2 獲取所有本類的的方法(不問訪問權限)
        Method[] mMethods = mClass.getDeclaredMethods();

        // 遍歷輸出所有變量信息:public protected private
        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 [] exceptionsType = method.getExceptionTypes();
            if (exceptionsType.length==0){
                System.out.println(")");
                System.out.println();
            }else{
                for (Class c:exceptionsType){
                    System.out.print(") throws "+ c.getName());
                }
                System.out.println();
            }

        }
    }

輸出日誌

獲取類的所有變量信息: Begin
子類的名稱: 
com.java.basic.reflect.SonClass
private void setSonAge(int arg0,)

public void printSonMsg()

private java.lang.String getSonName()

private int getSonAge()

private void setSonName(java.lang.String arg0,)

獲取類的所有變量信息: End

三、訪問或操作類的私有變量和方法

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

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

請注意看測試類中變量和方法的修飾符(訪問權限

TestClass.java

package com.java.basic.reflect;

/**
 * 測試Java反射獲取類的私有變量
 */
public class TestClass {

    /**
     * 私有常量
     */
    private String MSG = "Original";

    /**
     * 公共常量
     */
    public String MSG_PUBLIC = "ORIGINAL_PUBLIC";

    /**
     * 受保護常量()
     */
    protected  String MSG_PROTECTED = "MSG_PROTECTED";

    /**
     * 私有方法
     * @param head
     * @param tail
     */
    private void privateMethod(String head, int tail){
        System.out.println(head+" "+tail);
    }

    /**
     * 公共訪問方法
     * @return
     */
    public String getMsg(){
        return MSG;
    }
}

3.1 訪問私有方法

以訪問 TestClass 類中的私有方法 privateMethod(...) 爲例,方法加參數是爲了考慮最全的情況,很貼心有木有?先貼代碼,看註釋,最後我會重點解釋部分代碼。

    /**
     * 訪問對象的私有方法
     * 爲簡潔代碼,在方法上拋出總的異常,實際開發別這樣
     */
    @Test
    public void getPrivateMethod()throws Exception{

        /**
         * 獲取TestClass類的實例
         */
        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 com.java.basic.reflect.test.ReflectTest can not access a member of class com.java.basic.reflect.TestClass with modifiers "private"

	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Method.invoke(Method.java:491)
	at com.java.basic.reflect.test.ReflectTest.getPrivateMethod(ReflectTest.java:219)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

正常運行後,打印如下,調用私有方法成功:

獲取類的所有變量信息: Begin
Java Reflect  666
獲取類的所有變量信息: End

3.2 修改私有變量

以修改 TestClass 類中的私有變量 MSG 爲例,其初始值爲 "Original" ,我們要修改爲 "Modified"。老規矩,先上代碼看註釋。

/**
         * 修改對象私有變量的值
         * 爲簡潔代碼,在方法上拋出總的異常
         */
        @Test
        public void modifyPrivateFiled(){
            // 獲取CLass類的實例
            TestClass testClass = new TestClass();
            Class mClass = testClass.getClass();

            try {
                // 獲取私有變量
                Field privateField = mClass
                        .getDeclaredField("MSG");

                // 操作私有變量
                if(privateField != null){
                    // 獲取私有變量的訪問權限
                    privateField.setAccessible(true);

                    // 打印修改之前的變量值
                    System.out.println("修改之前的變量值爲: "+testClass.getMsg());

                    /**
                     * 調用set(object, value)修改私有變量的值
                     * privateFiled:獲取到的私有變量
                     * testClass: 要操作的變量
                     * "Modified": 要修改成的值
                     */
                    // 執行修改並打印修改之後的值
                    privateField.set(testClass,"Modified");
                    System.out.println("MSG修改之後的打印: "+testClass.getMsg());
                }
            }catch (Exception ex){
                ex.printStackTrace();
            }
    }

此處代碼和訪問私有方法的邏輯差不多,就不再贅述,從輸出信息看出 修改私有變量 成功:

獲取類的所有變量信息: Begin
修改之前的變量值爲: Original
MSG修改之後的打印: Modified
獲取類的所有變量信息: End

 

3.2 修改私有常量

在 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 文件已經將常量優化爲具體的值,在運行階段就直接使用具體的值了,所以即使修改了常量的值也已經毫無意義了

//String 會被 JVM 優化
    private final String FINAL_VALUE = "FINAL";

    public String getFinalValue(){
        //劇透,會被優化爲: return "FINAL" ,拭目以待吧
        return FINAL_VALUE;
    }
    

接下來,是修改常量的值,先上代碼,請仔細看註釋:

    /**
     * 修改對象私有常量的值
     * 爲簡潔代碼,在方法上拋出總的異常,實際開發別這樣
     */
    @Test
    public void modifyFinalFiled() throws Exception{
        // 獲取Class類實例
        TestClass testClass = new TestClass();
        Class mCLass = testClass.getClass();

        // 獲取私有常量
        Field privateFinalFiled = mCLass.getDeclaredField("FINAL_VALUE");

        // 執行常量值的修改
        if(privateFinalFiled != null){
            // 獲取私有常量的訪問權限
            privateFinalFiled.setAccessible(true);

            // 打印修改之前的值
            System.out.println("私有常量修改之前的值爲: "+privateFinalFiled.get(testClass));

            // 修改私有常量的值
            privateFinalFiled.set(testClass,"modified");

            //調用 finalField 的 getter 方法
            //輸出 FINAL_VALUE 修改後的值
            System.out.println("常量修改之後的值爲: "+privateFinalFiled.get(testClass));

            //使用對象調用類的 getter 方法
            //獲取值並輸出
            System.out.println("Actually :FINAL_VALUE = "
                    + testClass.getFinalValue());
        }
    }

上面的代碼不解釋了,註釋巨詳細有木有!特別注意一下第3步的註釋,然後來看看輸出,已經迫不及待了,擦亮雙眼:

獲取類的所有變量信息: Begin
私有常量修改之前的值爲: FINAL
常量修改之後的值爲: modified
Actually :FINAL_VALUE = FINAL
獲取類的所有變量信息: End

結果出來了:

第一句打印修改前 FINAL_VALUE 的值,沒有異議;

第二句打印修改後常量的值,說明FINAL_VALUE確實通過反射修改了;

第三句打印通過 getFinalValue() 方法獲取的 FINAL_VALUE 的值,但還是初始值,導致修改無效!

這結果你覺得可信嗎?什麼,你還不信?問我怎麼知道 JVM 編譯後會優化代碼?那要不這樣吧,一起來看看 TestClass.java 文件編譯後得到的 TestClass.class 文件。爲避免說代碼是我自己手寫的,我決定不粘貼代碼,直接截圖:

TestClass.class æ件

 

看到了吧,有圖有真相,getFinalValue() 方法直接 return "FINAL"!同時也說明了,程序運行時是根據編譯後的 .class 來執行的

順便提一下,如果你有時間,可以換幾個數據類型試試,正如上面說的,有些數據類型是不會優化的。你可以修改數據類型後,根據我的思路試試,看輸出覺得不靠譜就直接看 .classs 文件,一眼就能看出來哪些數據類型優化了 ,哪些沒有優化。下面說下一個知識點。(關於這種情況在接下來的時間中可以新開一篇博客討論JVM編譯時的常量優化請跨國)

02. 想辦法也要修改!

不能修改,這你能忍?彆着急,不知你發現沒,剛纔的常量都是在聲明時就直接賦值了。你可能會疑惑,常量不都是在聲明時賦值嗎?不賦值不報錯?當然不是啦。

方法一

事實上,Java 允許我們聲明常量時不賦值,但必須在構造函數中賦值。你可能會問我爲什麼要說這個,這就解釋:

我們修改一下 TestClass 類,在聲明常量時不賦值,然後添加構造函數併爲其賦值,大概看一下修改後的代碼:

package com.java.basic.reflect;

/**
 * 測試Java反射獲取類的私有變量
 */
public class TestClass {

    /**
     * 私有常量
     */
    private String MSG = "Original";

    /**
     * 公共常量
     */
    public String MSG_PUBLIC = "ORIGINAL_PUBLIC";

    /**
     * 受保護常量()
     */
    protected  String MSG_PROTECTED = "MSG_PROTECTED";

    /**
     * 私有方法
     * @param head
     * @param tail
     */
    private void privateMethod(String head, int tail){
        System.out.println(head+" "+tail);
    }

    /**
     * 公共訪問方法
     * @return
     */
    public String getMsg(){
        return MSG;
    }

    /**
     * test 1 begin
     */
    //String 會被 JVM 優化
//    private final String FINAL_VALUE = "FINAL";
//
//    public String getFinalValue(){
//        //劇透,會被優化爲: return "FINAL" ,拭目以待吧
//        return FINAL_VALUE;
//    }
    /**
     * test 1 END
     */

    // 聲明一個常量但是不賦值(在構造函數中賦值)
    private final String FINAL_VALUE;

    public TestClass(){
        this.FINAL_VALUE = "FINAL";
    }

    public String getFinalValue(){
        //劇透,會被優化爲: return "FINAL" ,拭目以待吧
        return FINAL_VALUE;
    }

}

PS: 在將目標常量稍作調整之後再運行測試代碼看看是什麼情況(爲了方便閱讀,這裏將測試代碼再貼一次):

 

    /**
     * 修改對象私有常量的值
     * 爲簡潔代碼,在方法上拋出總的異常,實際開發別這樣
     */
    @Test
    public void modifyFinalFiled() throws Exception{
        // 獲取Class類實例
        TestClass testClass = new TestClass();
        Class mCLass = testClass.getClass();

        // 獲取私有常量
        Field privateFinalFiled = mCLass.getDeclaredField("FINAL_VALUE");

        // 執行常量值的修改
        if(privateFinalFiled != null){
            // 獲取私有常量的訪問權限
            privateFinalFiled.setAccessible(true);

            // 打印修改之前的值
            System.out.println("私有常量修改之前的值爲: "+privateFinalFiled.get(testClass));

            // 修改私有常量的值
            privateFinalFiled.set(testClass,"modified");

            //調用 finalField 的 getter 方法
            //輸出 FINAL_VALUE 修改後的值
            System.out.println("常量修改之後的值爲: "+privateFinalFiled.get(testClass));

            //使用對象調用類的 getter 方法
            //獲取值並輸出
            System.out.println("Actually :FINAL_VALUE = "
                    + testClass.getFinalValue());
        }
    }

運行之後的結果爲: 

獲取類的所有變量信息: Begin
私有常量修改之前的值爲: FINAL
常量修改之後的值爲: modified
Actually :FINAL_VALUE = modified
獲取類的所有變量信息: End

想知道爲啥,還得看編譯後的 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. 到底能不能改?

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

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

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

å¤æ­è½ä¸è½æ¹

以上關於Java反射的使用是不是很精彩,條理清晰,論證明確。不得不說這真的是一個大神,能把Java中相對高級的知識點講的這麼通俗易懂的人真的不多。假如有小夥伴想學習這個大神的文章請點此連接: https://juejin.im/post/598ea9116fb9a03c335a99a4 個人決定學習反射此文足矣。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章