final屬性值能被反射修改嗎?

weixin_34162695發表於2018-12-07

在剛哥的知識星球中,看到有網友詢問反射給final修飾的欄位設值,為啥設值會失敗,之前也深入學習一下反射,對這個問題有點迷惑,於是學習起來並寫demo實踐一下。

先建立一個Test類,裡面含有final修飾的變數

public class Test {
    private final String NAME = "亦袁非猿";
    
    public String getName() {
        return NAME;
    }
}

然後通過反射修改

public class Client {
    public static void main(String[] args) {
        Test test = new Test();
        Class mClass = test.getClass();
        // 獲取NAME變數進行修改
        Field field = mClass.getDeclaredField("NAME");
        if (field != null) {
           field.setAccessible(true);
           System.out.println("modify before "+field.get(test));
           // 進行修改 
           field.set(test, "鋼絲");
           System.out.println("modify after "+field.get(test));
           System.out.println("getName = "+test.getName());
         }
    }
}

// 輸出:
modify before 亦袁非猿
modify after 鋼絲
getName = 亦袁非猿

看上面的輸出,修改後再反射獲取修改的變數,是修改成功的,但是,呼叫方法獲取,發現返回還是修改前的值,為啥呢?

這裡要涉及到Java虛擬機器,在Java類載入階段的準備階段,會對被final修飾的屬性做優化。在編譯期被優化後的Test.class如下:

public class Test {
    private final String NAME = "亦袁非猿";
    
    public String getName() {
        // 可以發現,在class中,NAME被直接優化變成"亦袁非猿"了
        return "亦袁非猿";
    }
}

所以,通過反射修改NAME的值,再呼叫該方法也修改無效。要注意一點,NAME是可以被反射修改且修改成功了。

上面說到,只有基本資料型別和String型別才會做優化導致修改無效。對於包裝類或者物件型別,還是可以修改成功的,不信,看下面的程式碼

private final String JOB = new String("安卓程式設計師");

// 進行反射修改
Field jobField = mClass.getDeclaredField("JOB");
if (jobField != null) {
    jobField.setAccessible(true);
    System.out.println("modify before "+jobField.get(test));
    jobField.set(test, new String("大前端程式設計師"));
    System.out.println("modify after "+jobField.get(test));
    System.out.println("getJob = "+test.getJOB());
}

// 輸出:
modify before 安卓程式設計師
modify after 大前端程式設計師
getJob = 大前端程式設計師

看到輸出可以發現,final被賦值後,還是可以通過反射重新賦值的。包裝類也是,如下程式碼

private final Integer AGE = new Integer(18);

// 進行反射修改
Field ageFiled = mClass.getDeclaredField("AGE");
if (ageFiled != null) {
    ageFiled.setAccessible(true);
    System.out.println("modify before "+ageFiled.get(test));
    ageFiled.set(test, new Integer(19));
    System.out.println("modify after "+ageFiled.get(test));
    System.out.println("getAge = "+test.getAge());
}

// 輸出:
modify before 18
modify after 19
getAge = 19

繼續看Test.class檔案,看看被編譯後的內容,沒有被優化替換掉

public class Test {
    private final String JOB = new String("安卓程式設計師");
    private final Integer AGE = new Integer(18);
    
    public Integer getAge() {
        return this.AGE;
    }

    public String getJOB() {
        return this.JOB;
    }
}

另外,final型別定義後,不一定需要立馬賦值,可以在構件函式進行初始化,那麼,能否修改呢?said is null,直接上程式碼。

public class Test {
    private final String LOCATION;
    
    public Test() {
        LOCATION = "廣州";
    }
    
    public String getLocation() {
        return LOCATION;
    }
}

// 通過反射修改
Field locationField = mClass.getDeclaredField("LOCATION");
if (field != null) {
    locationField.setAccessible(true);
    System.out.println("modify before "+locationField.get(test));
    locationField.set(test, "深圳");
    System.out.println("modify after "+locationField.get(test));
    System.out.println("getName = "+test.getLocation());
}

// 輸出:
modify before 廣州
modify after 深圳
getName = 深圳

同樣還是修改有效的,繼續看Test.class檔案,發現被優化後,建構函式的值,被直接移動到final的定義中,方法返回的是LOCATION的引用。

public class Test {
    private final String LOCATION = "廣州";

    public Test() {
    }

    public String getLocation() {
        return this.LOCATION;
    }
}

總結

回到一開始的問題,final屬性值能否被修改呢?這個就要看final修飾變數的型別以及初始化的時機,通過看編譯後的class檔案就可以知道了。

1839046-d8f7b1a0463eaa97.png

相關文章