最近由於學習編寫 Android應用程式 所以再次學習 Java
發現一些功能不能直接使用,而是需要使用 映射(Reflection) 來執行
在下因此尋找 映射 資料
預覽
發現一些功能不能直接使用,而是需要使用 映射(Reflection) 來執行
在下因此尋找 映射 資料
Java 的一般情況下,當 類別(Class) 或 方法(Method) 或 屬性(Field) 以 私有(private) 的情況下宣告
其他類別都不能存取該資料
但當使用 (映射)Reflection 時,即使使用 私有 ,仍能被存取
其他類別都不能存取該資料
但當使用 (映射)Reflection 時,即使使用 私有 ,仍能被存取
獲取私有靜態屬性
public class MyClass { private static boolean MY_BOOLEAN = true; }
靜態(static) 屬性屬於 類別 ,能以 Class.Field 的方式存取,但設定為 私有 時便無法讓其他 類別 存取
public class Test { public static void main(String[] args) throws Exception { System.out.println(MyClass.MY_BOOLEAN); } }
測試靜態屬性
編譯結果:
Test.java:3: error: MY_BOOLEAN has private access in MyClass System.out.println(MyClass.MY_BOOLEAN); ^ 1 error
由於 MY_BOOLEAN 為 私有,因此 MY_BOOLEAN 只能在 MyClass 中使用,導致編譯出錯
但如果使用 映射 時,便可以存取 私有屬性:
import java.lang.reflect.Field; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Field field = clazz.getDeclaredField("MY_BOOLEAN"); field.setAccessible(true); System.out.println(field.get(null)); } }
field.setAccessible(true) 令 私有屬性 能被存取
再使用 field.get() 便能獲取 私有屬性 的資料
再使用 field.get() 便能獲取 私有屬性 的資料
true
不過 field.get() 只會傳回 Object類別,而輸出內容只會將物件的 toString 顯示,並不能確定型態
例如:
例如:
public class MyClass { private static boolean PRIMITIVE_BOOLEAN = true; private static Boolean CLASS_BOOLEAN = true; private static String CLASS_String = "true"; }
import java.lang.reflect.Field; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Field[] fields = { clazz.getDeclaredField("PRIMITIVE_BOOLEAN"), clazz.getDeclaredField("CLASS_BOOLEAN"), clazz.getDeclaredField("CLASS_String"), }; for (Field field : fields) { field.setAccessible(true); System.out.println(field.get(null)); } } }
執行結果:
true true true
所有資料輸出都只會顯示 true ,不能確認傳回是 原始boolean 或 Boolean類別 或 String類別
因此還需要使用 field.getType().getName() 的確定型態
因此還需要使用 field.getType().getName() 的確定型態
import java.lang.reflect.Field; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Field[] fields = { clazz.getDeclaredField("PRIMITIVE_BOOLEAN"), clazz.getDeclaredField("CLASS_BOOLEAN"), clazz.getDeclaredField("CLASS_String"), }; for (Field field : fields) { field.setAccessible(true); System.out.println(field.getType().getName()); } } }
執行結果:
boolean java.lang.Boolean java.lang.String
確認類型後,便可以 強行變更型態 來執行其他操作
既然能夠獲取 私有屬性 ,因此 封裝(<default>) 及 保護(protected) 的資料亦能夠相同方法獲取
因此不重覆測試
因此不重覆測試
更改私有靜態屬性
使用 映射 除了能夠獲取 屬性,還可以使用 field.set() 來修改 屬性
import java.lang.reflect.Field; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Field field = clazz.getDeclaredField("PRIMITIVE_BOOLEAN"); field.setAccessible(true); System.out.println(field.get(null)); field.set(null, false); System.out.println(field.get(null)); } }
執行結果:
true false
獲取及更改物件私有屬性
public class MyClass { private boolean myBoolean = true; }
沒有 static 的屬性不再是靜態屬性,不能直接以類別方式存取
而是需要先建立該類別的物件,再在物件中存取
而是需要先建立該類別的物件,再在物件中存取
import java.lang.reflect.Field; public class Test { public static void main(String[] args) throws Exception { MyClass myClass = new MyClass(); Class clazz = myClass.getClass(); Field field = clazz.getDeclaredField("myBoolean"); field.setAccessible(true); System.out.println(field.get(myClass)); field.set(myClass, false); System.out.println(field.get(myClass)); } }
使用 field.get() 時,需要指定物件參數,才能存取該物件對應的屬性
使用私有靜態功能
public class MyClass { private static void myMethod() { System.out.println("I am private static method."); } }
與先前的例子相同,私有方法 不能被其他類別存取
import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Method method = clazz.getDeclaredMethod("myMethod"); method.setAccessible(true); method.invoke(null); } }
與 field.get() 相似,不過改為使用 method.invoke()
Note: Test.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details.
編譯時,由於 clazz.getDeclaredMethod() 有機會出現不安全的操作,因此會出現提示,但仍然能夠編譯
如果想避免編譯時出現提示,可以將 Test類別 修改成:
如果想避免編譯時出現提示,可以將 Test類別 修改成:
import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; @SuppressWarnings("unchecked") Method method = clazz.getDeclaredMethod("myMethod"); method.setAccessible(true); method.invoke(null); } }
執行結果:
I am private static method.
由於 method.invoke() 不會區分 void 或 傳回資料
public class MyClass { private static void voidMethod() { return; } private static String nullMethod() { return null; } private static String stringMethod() { return "null"; } }
import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; @SuppressWarnings("unchecked") Method[] methods = { clazz.getDeclaredMethod("voidMethod"), clazz.getDeclaredMethod("nullMethod"), clazz.getDeclaredMethod("stringMethod"), }; for (Method method : methods) { method.setAccessible(true); System.out.println(method.getReturnType().getName()); System.out.println(method.invoke(null)); } } }
執行結果:
void null java.lang.String null java.lang.String null
使用 method.invoke() 時,即使功能是 void 仍會傳回 null 資料
與屬性相似,使用 method.getReturnType().getName() 來確認傳回類型
與屬性相似,使用 method.getReturnType().getName() 來確認傳回類型
使用私有物件功能
public class MyClass { private void objectMethod() { System.out.println("I am private object method."); } }
import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { MyClass myClass = new MyClass(); Class<MyClass> clazz = myClass.getClass(); @SuppressWarnings("unchecked") Method method = clazz.getDeclaredMethod("objectMethod"); method.setAccessible(true); method.invoke(myClass); } }
以 映射 使用物件的功能 與獲取物件的資料相似
使用 method.invoke() 時,同樣需要指定物件參數,才能使用該物件對應的方法
使用 method.invoke() 時,同樣需要指定物件參數,才能使用該物件對應的方法
使用附有參數的功能
public class MyClass { private static int square(int value) { return value * value; } }
import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; @SuppressWarnings("unchecked") Method method = clazz.getDeclaredMethod("square", int.class); method.setAccessible(true); System.out.println(method.invoke(null, 16)); } }
使用 映射 時,由於 方法 允許 重載(Overload)
因此在 clazz.getDeclaredMethod() 除了傳入功能名稱的參數外,可以因應參數的類型(或以陣列)順序傳入以使用對應重載的 方法
最後在 method.invoke() 除了第一個參數傳入 null 或物件外,亦需要將對應的參數類型的資料(或以陣列)順序傳入
因此在 clazz.getDeclaredMethod() 除了傳入功能名稱的參數外,可以因應參數的類型(或以陣列)順序傳入以使用對應重載的 方法
最後在 method.invoke() 除了第一個參數傳入 null 或物件外,亦需要將對應的參數類型的資料(或以陣列)順序傳入
使用私有建構子建立物件
如果類別只有私有 建構子(Constructor),又沒有靜態方法或靜態屬性獲取該類別的物件,即是該類別是無法以正規方法建立物件
但透過 映射 就能夠將建立該類別的物件
但透過 映射 就能夠將建立該類別的物件
public class MyClass { private MyClass(int edge) { System.out.println(edge * 4); } }
import java.lang.reflect.Constructor; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Constructor<MyClass> constructor = clazz.getDeclaredConstructor(int.class); constructor.setAccessible(true); MyClass myClass = constructor.newInstance(8); } }
clazz.getDeclaredConstructor() 的使用方法與 clazz.getDeclaredMethod() 相似,只是不需要傳入名稱
如果需要附加參數,同樣將參數的類型順序傳入,再在 constructor.newInstance() 順序傳入對應資料即可
如果需要附加參數,同樣將參數的類型順序傳入,再在 constructor.newInstance() 順序傳入對應資料即可
使用私有靜態內部類別的方法
內部類別(Inner Class) 與一般類別相同,只是內部類別存在於一個類別中
而且內部類別允許以私有方式製作
而且內部類別允許以私有方式製作
public class MyClass { private static class MySubClass { private MySubClass() { } private void mySubMethod() { System.out.println(this.getClass().getName()); } } }
import java.lang.reflect.Constructor; import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class clazz = Class.forName("MyClass$MySubClass"); Constructor constructor = clazz.getDeclaredConstructor(); constructor.setAccessible(true); Object innerObject = constructor.newInstance(); Class innerClass = innerObject.getClass(); Method innerMethod = innerClass.getDeclaredMethod("mySubMethod"); innerMethod.setAccessible(true); innerMethod.invoke(innerObject); } }
執行結果:
MyClass$MySubClass
由於 私有內部類別 無法被匯入,因此物件只能以 Object類別 建立
但 Object類別 不會擁有 內部類別的 屬性 或 方法
而且由於無法強制轉變類型,即使是 公有(public) 的 屬性 或 方法 都不能直接使用
整過操作程式,都需要使用 映射 來使用方法或獲取屬性
但 Object類別 不會擁有 內部類別的 屬性 或 方法
而且由於無法強制轉變類型,即使是 公有(public) 的 屬性 或 方法 都不能直接使用
整過操作程式,都需要使用 映射 來使用方法或獲取屬性
另外,內部類別 的名稱使用 $(錢符號(dollar)) 連接,而非 .(點(Period)) 來連接
使用私有內部類別物件的方法
一般情況很少會設計私有內部類別物件,但技術上是允許這種設計
public class MyClass { private MyClass() { } private class MySubClass { private MySubClass() { } private void mySubMethod() { System.out.println(this.getClass().getName()); } } }
import java.lang.reflect.Constructor; import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class<MyClass> outerClass = MyClass.class; Constructor<MyClass> outerConstructor = outerClass.getDeclaredConstructor(); outerConstructor.setAccessible(true); MyClass myClass = outerConstructor.newInstance(); Class innerClass = Class.forName("MyClass$MySubClass"); Constructor innerConstructor = innerClass.getDeclaredConstructor(MyClass.class); innerConstructor.setAccessible(true); Object innerObject = innerConstructor.newInstance(myClass); Method innerMethod = innerClass.getDeclaredMethod("mySubMethod"); innerMethod.setAccessible(true); innerMethod.invoke(innerObject); } }
執行結果:
MyClass$MySubClass
如果將例子中所有 private 改成 public
MyClass myClass = new MyClass(); MyClass.MySubClass mySubClass = myClass.new MySubClass(); mySubClass.mySubMethod();
語法上已經有點麻煩,如果必須以 映寫 方法建立物入更加繁複的步驟
更改物件常數屬性
宣告以 常數(final) 的變數,其地址只能被指派資料一次
class MyClass { public final int VALUE = 0; }
public class Test { public static void main(String[] args) throws Exception { MyClass myClass = new MyClass(); myClass.VALUE = 1; } }
執行結果:
Test.java:4: error: cannot assign a value to final variable VALUE myClass.VALUE = 1; ^ 1 error
當地址被重覆指派時,編譯時會出現錯誤
但使用 映射 仍能透過 映射 更改
import java.lang.reflect.Constructor; import java.lang.reflect.Field; public class Test { public static void main(String[] args) throws Exception { MyClass myClass = new MyClass(); Field field = clazz.getDeclaredField("VALUE"); field.setAccessible(true); System.out.println(field.get(myClass)); field.set(myClass, 1); System.out.println(field.get(myClass)); } }
執行結果:
0 1
更改靜態常數屬性
使用 映射 更改靜態常數屬性則比較麻煩
public class MyClass { public static final int VALUE = 0; }
import java.lang.reflect.Field; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Field field = clazz.getDeclaredField("VALUE"); field.setAccessible(true); System.out.println(field.get(null)); field.set(null, 1); System.out.println(field.get(null)); } }
執行結果:
0 Exception in thread "main" java.lang.IllegalAccessException: Can not set static final int field MyClass.VALUE to java.lang.Integer at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:76) at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:80) at java.base/jdk.internal.reflect.UnsafeQualifiedStaticIntegerFieldAccessorImpl.set(UnsafeQualifiedStaticIntegerFieldAccessorImpl.java:77) at java.base/java.lang.reflect.Field.set(Field.java:780) at Test.main(Test.java:8)
使用類似更改物件常數屬性的方法更改靜態常數屬性會出現錯誤
編譯時顯示 不能更改靜態常數屬性
編譯時顯示 不能更改靜態常數屬性
要更改類別靜態屬性需要先將更改 屬性 的 修飾(Modifier) ,將 常數位元取消 才能更改
import java.lang.reflect.Field; import java.lang.reflect.Modifier; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Field field = clazz.getDeclaredField("VALUE"); field.setAccessible(true); Class modifiersClass = field.getClass(); Field modifiersField = modifiersClass.getDeclaredField("modifiers"); modifiersField.setAccessible(true); // remove final modifier modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL); System.out.println(field.get(null)); field.set(null, 1); // gain final modifier modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL); System.out.println(field.get(null)); } }
執行結果:
WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by Test (file:/path/of/file/) to field java.lang.reflect.Field.modifiers WARNING: Please consider reporting this to the maintainers of Test WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release 0 1
但即是能夠修改,執行程式時,會出現警告資訊
補充資料
在測試各種以 映射 更改存取資料的操作中,會發現當使用 私有方法 會出現 不安全提示 及 更改 靜態常數屬性 會出現 警告
由於 私有方法 有機會是 讓內部其他 方法 使用,當中如果沒有檢查資料,便會出現非預期結果
public class MyClass { private static int privateDivision(int dividend, int divider) { return dividend / divider; } public static void publicDivision(int dividend, int divider) { if (divider == 0) { System.out.println("Cannot divided by 0"); } else { System.out.println(MyClass.privateDivision(dividend, divider)); } } }
public class Test { public static void main(String[] args) throws Exception { MyClass.publicDivision(1, 0); MyClass.publicDivision(1, 1); } }
執行結果:
Cannot divided by 0 1
由於 publicDivision() 在運算有檢查傳入的參數的況狀來避免執行時出現錯誤
import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class clazz = MyClass.class; Method method = clazz.getDeclaredMethod("privateDivision", int.class, int.class); method.setAccessible(true); System.out.println(method.invoke(null, 1, 0)); System.out.println(method.invoke(null, 1, 1)); } }
執行結果:
Exception in thread "main" java.lang.reflect.InvocationTargetException at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:119) at java.base/java.lang.reflect.Method.invoke(Method.java:578) at Test.main(Test.java:7) Caused by: java.lang.ArithmeticException: / by zero at MyClass.privateDivision(Test.java:7) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ... 2 more
由於 privateDivision() 在運算沒有檢查傳入的參數的況狀,執行時會出現異常情況
更改其他類別常數屬性,是非常危險
import java.lang.reflect.Field; import java.lang.reflect.Modifier; public class Test { public static void main(String[] args) throws Exception { Class clazz = Boolean.class; Field field = clazz.getDeclaredField("TRUE"); field.setAccessible(true); Class modifiersClass = field.getClass(); Field modifiersField = modifiersClass.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL); System.out.println(Boolean.TRUE); field.set(null, false); System.out.println(Boolean.TRUE); modifiersField.set(field, field.getModifiers() & ~Modifier.FINAL); } }
執行結果:
WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by Test (file:/path/of/file/) to field java.lang.reflect.Field.modifiers WARNING: Please consider reporting this to the maintainers of Test WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release true false
如同上述例子,將 Boolean類別 的 TRUE靜態常數屬性 更改為 false
當其他類別需要使用 Boolean.TRUE 時,便會出現嚴重問題
當其他類別需要使用 Boolean.TRUE 時,便會出現嚴重問題
不過在 JDK 12+ 基於安全考慮,已經禁止存取 Field類別 的 modifiers屬性
但仍然能夠使用 sun.misc.Unsafe 類別 來更改 靜態屬性 ,不過比之前的方法更加複雜
但仍然能夠使用 sun.misc.Unsafe 類別 來更改 靜態屬性 ,不過比之前的方法更加複雜
import java.lang.reflect.Field; import java.lang.reflect.Method; public class Test { public static void main(String[] args) throws Exception { Class booleanClass = Boolean.class; Field fieldClass = booleanClass.getDeclaredField("TRUE"); Class unsafeClass = Class.forName("sun.misc.Unsafe"); Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Object unsafeObject = unsafeField.get(null); Method unsafeMethodBase = unsafeClass.getDeclaredMethod("staticFieldBase", fieldClass.getClass()); Object unsafeMethodBaseObject = unsafeMethodBase.invoke(unsafeObject, fieldClass); Method unsafeMethodOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", fieldClass.getClass()); Object unsafeMethodOffsetObject = unsafeMethodOffset.invoke(unsafeObject, fieldClass); Method unsafeMethodPutObject = unsafeClass.getDeclaredMethod("putObject", Object.class, long.class, Object.class); System.out.println(Boolean.TRUE); unsafeMethodPutObject.invoke(unsafeObject, unsafeMethodBaseObject, unsafeMethodOffsetObject, false); System.out.println(Boolean.TRUE); } }
sun.misc.Unsafe 在 JDK 7 開始出現,但 不屬於標準 JDK ,因此無法找到 sun.misc.Unsafe 的 API
sun.misc.Unsafe 能存取記憶體及其他低階操作
由於 sun.misc.Unsafe 能夠繞過 JVM 的安全設定,如果使用不當,會影響 JVM 的穩定性或產生不安全操作
不建議在一般開發中胡亂使用
由於 sun.misc.Unsafe 能夠繞過 JVM 的安全設定,如果使用不當,會影響 JVM 的穩定性或產生不安全操作
不建議在一般開發中胡亂使用
在下為了將來可以方便使用這些操作,而編寫一些簡化操作的方法
public static<T> T createObject(Class<T> clazz, Class[] parameterClasses, Object[] parameterValues) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { Constructor<T> constructor = clazz.getDeclaredConstructor(parameterClasses); constructor.setAccessible(true); return constructor.newInstance(parameterValues); }
public static Object invokeMethod(Class clazz, String methodName, Class[] parameterClasses, Object[] parameterValues, Object instance) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { Method method = clazz.getDeclaredMethod(methodName, parameterClasses); method.setAccessible(true); return method.invoke(instance, parameterValues); }
public static Object getValue(Class clazz, String fieldName, Object instance) throws NoSuchFieldException, IllegalAccessException { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); return field.get(instance); }
public static void setValue(Class clazz, String fieldName, Object value, Object instance) throws NoSuchFieldException, IllegalAccessException { Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); int modifiers = field.getModifiers(); if ((modifiers & Modifier.FINAL) > 0) { Class modifiersClass = field.getClass(); Field modifiersField = modifiersClass.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.set(field, modifiers & ~Modifier.FINAL); field.set(instance, value); modifiersField.set(field, modifiers & ~Modifier.FINAL); } else { field.set(instance, value); } }
public static void setStaticValue(Class clazz, String fieldName, Object value) throws NoSuchFieldException, ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { Field field = clazz.getDeclaredField(fieldName); Class unsafeClass = Class.forName("sun.misc.Unsafe"); Field unsafeField = unsafeClass.getDeclaredField("theUnsafe"); unsafeField.setAccessible(true); Object unsafeObject = unsafeField.get(null); Method unsafeMethodBase = unsafeClass.getDeclaredMethod("staticFieldBase", field.getClass()); Object unsafeMethodBaseObject = unsafeMethodBase.invoke(unsafeObject, field); Method unsafeMethodOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", field.getClass()); Object unsafeMethodOffsetObject = unsafeMethodOffset.invoke(unsafeObject, field); Method unsafeMethodPutObject = unsafeClass.getDeclaredMethod("putObject", Object.class, long.class, Object.class); unsafeMethodPutObject.invoke(unsafeObject, unsafeMethodBaseObject, unsafeMethodOffsetObject, false); }
總結
編寫 Android 程式時,偶然發現一些功能,不是 公有方法 或 公有屬性
但這些功能在下希望能加入到自己編寫的 Android應用程式 中,因此尋找資料發現可以透過 映射 達至效果
但這些功能在下希望能加入到自己編寫的 Android應用程式 中,因此尋找資料發現可以透過 映射 達至效果
沒有留言 :
張貼留言