Java反射, 修改欄位值, 實例化對象, 繞過構造器來實例化對象 ...
反射修改欄位
咱們從最簡單的例子到難, 一步一步深入.
使用反射修改一個private修飾符的變數name
咱們回到主題, 先用反射來實現一個最基礎的功能吧.
其中待獲取的name如下:
public class Pojo { private StringBuilder name = new StringBuilder("default"); public void printName() { System.out.println(name); } }
接下來咱們 使用反射來修改上面name的值.
為什麼要用反射呢? 因為成員變數name是private修飾的, 而且沒有提供一個setter方法.沒有方法可以設置name的值.
雖然沒有一個對外開放的介面, 但是反射卻可以輕而易舉地做到:
Pojo p = new Pojo(); // 查看被修改之前的值 p.printName(); // 反射獲取欄位, name成員變數 Field nameField = p.getClass().getDeclaredField("name"); // 由於name成員變數是private, 所以需要進行訪問許可權設定 nameField.setAccessible(true); // 使用反射進行賦值 nameField.set(p, new StringBuilder("111")); // 列印查看被修改後的值 p.printName();
發現被修改成功, 結果如下:
使用反射修改一個final修飾符的變數name
剛纔使用反射成功修改了private修飾的變數, 那麼如果是final修飾的變數那麼還能否使用反射來進行修改呢? (因為正常的setter getter操作反正是做不到.)
聲明一個final修飾的name如下. 接下來使用反射來對它進行修改. 目的也就是使name指向一個新的StringBuilder對象.
public class Pojo2 { private final StringBuilder name = new StringBuilder("default2"); public void printName() { System.out.println(name); } }
咱們看看反射的威力吧, 它能修改final的欄位的指向.也就是讓name欄位指向一個新的地址.
Pojo2 p = new Pojo2(); // 查看被修改之前的值 p.printName(); // 反射獲取欄位, name成員變數 Field nameField = p.getClass().getDeclaredField("name"); // 由於name成員變數是private, 所以需要進行訪問許可權設定 nameField.setAccessible(true); // 使用反射進行賦值 nameField.set(p, new StringBuilder("111")); // 列印查看被修改後的值 p.printName();
發現設置成功, 結果如下:
使用反射修改一個final修飾符的String類型變數name
如果說同學們在看我這篇文章時, 在前面偷懶了, 或者是認為StringBuilder和String沒什麼大區別, 於是就在前面把我代碼里的StringBuilder都改為了String, 那麼大家的執行結果將會是一個意外結果.
也就是我前面的例子用StringBuilder就能成功, 如果都替換成了String, 使用反射也不能夠成功賦值.
為什麼呢?
在講解為什麼之前, 我這裡把這個問題重現一下:
把前面的StringBuilder替換為String後的Pojo3
public class Pojo3 { private final String name = "default3"; public void printName() { System.out.println(name); } }
使用反射嘗試著進行賦值:
Pojo3 p = new Pojo3(); p.printName(); Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true); nameField.set(p, "111"); p.printName();
發現賦值失敗, 結果如下:
再一次提問: 為什麼呢?
因為JVM在編譯時期, 就把final類型的String進行了優化, 在編譯時期就會把String處理成常量, 所以 Pojo3里的printName()方法, 就相當於:
public void printName() { System.out.println("default3"); }
其實name的值是賦值成功了, 只是printName()方法在JVM優化後就被寫死了, 所以無論name是否被正確修改為其他的值, printName始終都會列印"default3".
那麼怎麼知道name是不是真的被重新賦值成功了呢?
看下麵代碼:
Pojo3 p = new Pojo3(); Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true); // 使用反射向name進行重新賦值 nameField.set(p, "111"); // 再使用反射再把name值取出來 Object name = nameField.get(p); // 把取出來的name值進行列印 System.out.println(name.toString());
結果如下, 說明name變數確實被賦值成功.
那麼可能有同學就問了,final修飾的String在JVM編譯時就被處理為常量, 怎麼樣防止這種現象呢? 請看下麵講解
使用反射修改一個final修飾符的String類型變數name, 同時防止字元串在編譯時被處理為常量
使用一些手段讓final String類型的name的初始值經過一次運行才能得到, 那麼就不會在編譯時期就被處理為常亮了.
public class Pojo4 { // 防止JVM編譯時就把"default4"作為常量處理 private final String name = (null == null ? "default4" : ""); public void printName() { System.out.println(name); } }
運行測試的還是那段反射代碼:
Pojo4 p = new Pojo4(); p.printName(); Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true); nameField.set(p, "111"); p.printName();
結果如下, 發現確實成功了:
那麼有同學就會問了, 除了上面這種方法外, 還有什麼方法能防止JVM在編譯時就把final String的變數處理為常亮呢 ?
答: 嗯...只要是讓name的值經過運行才能獲得, 那麼就不會被處理為常量. 我再舉個程式例子吧.看下麵代碼:
public class Pojo5 { private final String name = new StringBuilder("default5").toString(); public void printName() { System.out.println(name); } }
還是那段反射的代碼, 運行
@Test public void test5() throws Exception { Pojo5 p = new Pojo5(); p.printName(); Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true); nameField.set(p, "111"); p.printName(); }
結果如下, OK
使用反射修改一個static修飾符的變數name
剛纔展示了使用反射來修改final修飾的欄位, 接下來就演示一下使用反射來修改static修飾的變數:
如下的一個static修飾的一個name變數.
public class Pojo6 { private static StringBuilder name = new StringBuilder("default6"); public void printName() { System.out.println(name); } }
還是那段反射代碼來進行測試:
Pojo6 p = new Pojo6(); p.printName(); Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true); nameField.set(p, new StringBuilder("111")); p.printName();
發現結果如下, 也可以設置成功.
使用反射修改final + static修飾符的變數name
一個同時被final和static修飾的變數如下所示:
public class Pojo7 { private final static StringBuilder name = new StringBuilder("default7"); public void printName() { System.out.println(name); } }
如果還是通過下麵這段反射代碼來進行修改name的值, 那麼就錯了!
Pojo7 p = new Pojo7(); p.printName(); Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true); nameField.set(p, new StringBuilder("111")); p.printName();
執行之後會報出如下異常, 因為反射無法修改同時被static final修飾的變數:
那到底能不能修改呢?
答案是能修改.
那怎麼樣修改呢?
思路是這樣的, 先通過反射把name欄位的final修飾符去掉.看如下代碼:
先把name欄位通過反射取出來, 這個和之前的步驟都一樣, 反射出來的欄位類型(Field)命名為'nameField'
Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true);
接下來再通過反射, 把nameField的final修飾符去掉:
Field modifiers = nameField.getClass().getDeclaredField("modifiers"); modifiers.setAccessible(true); modifiers.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
然後就可以正常對name欄位進行值的修改了.
nameField.set(p, new StringBuilder("111"));
最後別忘了再把final修飾符加回來:
modifiers.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL);
本例子中反射部分完整的代碼如下:
// 註釋的這段代碼這樣使用是錯誤的 // Pojo7 p = new Pojo7(); // p.printName(); // Field nameField = p.getClass().getDeclaredField("name"); // nameField.setAccessible(true); // nameField.set(p, new StringBuilder("111")); // p.printName(); Pojo7 p = new Pojo7(); p.printName(); Field nameField = p.getClass().getDeclaredField("name"); nameField.setAccessible(true); Field modifiers = nameField.getClass().getDeclaredField("modifiers"); modifiers.setAccessible(true); modifiers.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL); nameField.set(p, new StringBuilder("111")); modifiers.setInt(nameField, nameField.getModifiers() & ~Modifier.FINAL); p.printName();
結果如下, 表示修改正確:
本文中的所有代碼都在這裡: https://github.com/GoldArowana/K-Object/tree/master/src/test/java/reflect/field 裡面的TestField.java為主要反射代碼