--- title: 'Java 反射、Meta Programming' disqus: kyleAlien --- Java 反射、Meta Programming === ## Overview of Content 如有引用參考請詳註出處,感謝 :smile: **反射是運行 `Running Time`** 的時候才會創建、尋找類,或是方法、建構子、屬性 > **反射是 Java 被視為動態語言的關鍵**,在運行期借助 Reflection API 進行反射 在程序中一般的類是在編譯期間就確定 (泛型例外),而反射基置創建的物件是運行時才確定 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**深入探索 Java 反射:理解並使用 Class 類 | Constructor、Method、Field、Annotation 和反射泛型**](https://devtechascendancy.com/java-reflection-guide_class-object/) ::: [TOC] ## 認識 [Class 類](https://developer.android.com/reference/java/lang/Class) JVM 加載 `.class` 檔案之後會在 JVM 中建立一個 `Class 類`,而 Class 類是指在「**運行期間**」的「**物件**」,這個物件內部會有類所有的描述(包括 Source Code、Annotation、Header、Static Field、Field … 等等資訊) :::info * **為什麼要保留這些資訊?所有的語言都有嗎?** 不,並非所有的語言都會保留這些訊息(像 C 語言就沒有),像是更針對執行速度、在意應用大小的語言就不會保留這些資訊 > 這些訊息我們會稱之為 MetaData,而針對 MetaData 進行操作的行為就稱之為「`Meta Programming`」 而保留這些訊息的程式語言(像是我們說的 Java)就可以更具有拓展性、自由度 ::: ### class 檔案、Class 類差別? * **認識 `.class` 檔案**: 我們在 IDE 中撰寫的檔案是 `.java` 檔案,而這個 `.java` 檔案無法直接在應用中執行(JVM 無法直接加載 `.java` 檔案) JVM 可運行的檔案是 `.class` 檔案,而要產出這個檔案就是要經過「編譯」的動作,透過編譯後就可以將 `.java` 檔案轉成 `.class` ```mermaid graph LR subgraph 源碼編譯 j(.java 檔案) -.-> |編譯| c(.class 檔案) end ``` * **`.class` 檔案中的資訊** `.class` 文件是 Java 編譯器生成的二進制文件,包含了 JVM(Java 虛擬機)可以直接解讀和執行的字節碼… class 文件中包含以下幾個主要部分的資訊: | class 檔案中的資訊 | 概述 | | - | - | | `Magic Number` | 用於標識這是一個 Java 類文件,固定為 `0xCAFEBABE` | | `Version Info` | Java 類文件的版本號,包括次版本號、主版本號 | | `Constant Pool` | 包含類文件中用到的所有常量,包括字符串、類名、方法名、字段名… 等等;常量池在類文件中佔據了很大的一部分 | | `Access Flags` | 用於標識類或接口的訪問權限和屬性,例如這個類是否是 `public`、`final`、`abstract` 等 | | `This Class` | 當前類的名稱 | | `Super Class` | 這個類的超類(父類)的名稱,如果這個類是 `java.lang.Object`,則超類為空 | | `Interfaces` | 這個類實現的所有介面 | | `Fields` | 類中定義的所有字段的資訊,包括名稱、類型和訪問修飾符 | | `Methods` | 類中定義的所有方法的資訊,包括方法名、返回類型、參數列表、訪問修飾符和方法的字節碼 | | `Attributes` | 類的額外屬性,包括類層次結構、源文件名稱、註解、調試信息等等 | ```mermaid graph LR subgraph .class 內容 1 1(Magic Number) 2(Version Info) 3(Constant Pool) 4(Access Flags) 5(This Class) end subgraph .class 內容 2 6(Super Class) 7(Interfaces) 8(Fields) 9(Methods) 10(Attributes) end ``` * `Class 類`:請注意這個「類」這個關鍵字,這個類就是一個物件,這個物件會保存在 JVM 的方法區; 而 Class 類就是封裝了 `.class` 檔案中所對應的類的訊息,方便我們在運行期間可以讀取這些資訊 :::success * 更多有關 [**JVM 與類加載**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/) 的概念請點擊連結去深入了解 在這邊我們可以簡單地去認知,一個 Class 類在 JVM 中只會擁有一個實例(`instance`) ```mermaid graph TB subgraph Runtime c(.class 檔案) --> |類加載, Class 類| JVM JVM --> |實例化 class| 物件 end ``` ::: ### 取得 Class 類 * 這裡先說明如何透過程式取得 Class 類,之後章節再說明取得 Class 類之後可以做些什麼事… 我們可以透過以下三種方式來取得 `Class 類`,範例如下: 1. 透過指定「類名」直接取得指定的 Class 類 ```java= public static void main(String[] args) { System.out.println( "Thread.class.toString: " + Thread.class.toString() ); } ``` 2. 透過 Class#`forName` 方法,並輸入完整的類路徑來取得 Class 類 ```java= public static void main(String[] args) { try { Class<?> clz = Class.forName("java.lang.Thread"); System.out.println("Class.forName: " + clz.toString()); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } ``` 3. 透過實例物件(`instance`)的 `getClass()` 方法來取得 Class 類 ```java= public static void main(String[] args) { Thread t = new Thread(); Class<?> clzz = t.getClass(); System.out.println("Thread.getClass: " + clzz.toString()); } ``` > ![](https://i.imgur.com/8EkFOjF.png) ### 認識 Java 反射包 * Java 有提供一個標準包用來分析 MetaData 並使用,該包在 [**`java.lang.reflect`**](https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/package-summary.html) 中,該包提供了一組用於反射(`Reflection`)操作的類和界面 > 簡單來說反射就是:是一種允許程序在運行時檢查和修改其自身結構的功能 這些類和界面使得程序可以動態地獲取類的結構信息(如類名、方法、字段、構造函數等),並且可以在運行時調用方法、訪問字段和創建實例 * **反射的主要類** | 主要類 | 概述 | | - | - | | `Constructor` | 代表類的建構函數,提供方法來創建新實例,包括傳遞參數 | | `Method` | 提供方法來調用方法,包括傳遞參數、獲取返回值 | | `Field` | 提供方法來讀取和設置字段的值,無論字段是私有、保護還是公共的 | | `Array` | 提供靜態方法來動態創建和操作數組 | * **反射的主要界面** | 主要類 | 概述 | | - | - | | `Type` | 所有類型的公共界面 | | `InvocationHandler` | 用於處理代理實例上的方法調用 | | `GenericArrayType`(用來處理泛型) | 代表泛型數組類型 | | `ParameterizedType`(用來處理泛型) | 代表參數化類型 | | `TypeVariable`(用來處理泛型) | 代表類型變量,是泛型中的一部分 | | `WildcardType`(用來處理泛型) | 代表通配符類型,是泛型中的一部分 | ### 反射的注意事項 * 反射會消耗一定的系統資源,多少會影響應用效能 * 反射調用可 **忽略權限檢查**,可能會破壞封裝導置安全問題 * 另外,對於可設定混淆的應用(像是 Android App 應用),就要特別小心!有使用反射技巧的程式,要記得設定跳過混淆,否則會造成框架無法正常運行!! :::danger 因為很多框架會依賴反射機制,而混淆會導致 `.class` 類中保存的訊息與我們認知的訊息不同 ::: ## 類的建構器 [Constructor](https://developer.android.com/reference/java/lang/reflect/Constructor) 前面我們有介紹到 Class 類中有保存建構器(也就是構造函數的資訊),在這裡我們就可以透過建構器來實例化類別,而不用透過 `new` 關鍵字來獲得實例 ```mermaid graph LR 實例化 --> |使用| n(new 關鍵字) 實例化 --> |使用| r(MetaData 中的 Constructor) ``` ### 透過 Class 實例化:newInstance * 透過 Class#`newInstance()` 方法可以直接創建一個 **無參的建構函數的實例** 範例如下: 1. **目標類**:目標類為一個無參數的 `public` 建構函數 ```java= class Constructor_1 { public Constructor_1() { System.out.println("執行 Constructor_1 建構函數"); } void print() { System.out.println("Hello World"); } } ``` 2. **反射目標類**:透過 Class#`newInstance` 方法直接處實例化物件,實例化後就可以以一般操作物件的方式使用 ```java= public static void main(String[] args) { Class<Constructor_1> c1 = Constructor_1.class; try { Constructor_1 instance = c1.newInstance(); instance.print(); } catch (InstantiationException | IllegalAccessException e) { e.printStackTrace(); } } ``` > ![](https://i.imgur.com/CicQvXl.png) :::warning * **使用 `newInstance` 這種方式實例化物件時需要注意以下事項** * **只能實例化無參建構函數**,如果建構函數有參數,則會實例化失敗 * **無法實例化使用 `private` 關鍵字描述的建構函數**,否則會產生 `IllegalAccessException` 錯誤 > ![](https://i.imgur.com/TB52Dh8.png) ::: ### 取得類的 Constructor * 另外我們可以透過 Class 取得 `Constructor` 類,透過這個方法我們就可以取得 Class 類中對於建構函數所有的描述,並且可以透過它來實例化類別 > 「**透過 `Constructor` 物件來表示類的建構函數**」 | Class 取得 `Constructor` 的方式 | 概述說明 | | - | - | | `getConstructors()` | **取得所有公開的建構函數**(不包括父類) | | `getDeclaredConstructors()` | **取得所有建構函數**,限定於該類(不包括父類) | ### 取得類「自身全部」建構函數 * 以下範例為使用 `getDeclaredConstructors()` 方法取得 Constructor 物件並使用 範例如下: 1. **目標類**:該目標類中有兩個建構函數,一個是 `public` 的建構函數,以及另一個 `private` 描述的有參構造函數 ```java= class Constructor_2 { Constructor_2() { System.out.println("執行 Constructor_2 建構函數"); } private Constructor_2(int a) { System.out.println("執行 Constructor_2 建構函數, a = " + a); } void print() { System.out.println("Hello World"); } } ``` 2. **反射目標類**: * Constructor 實例化類別比較特殊,**它可以訪問私有建構函數**,不過必須透過 setAccessible() 設定為可放問(非私有的可以不用設定) ```java= public static void main(String[] args) { Class<Constructor_2> c2 = Constructor_2.class; Constructor<?>[] cons = c2.getDeclaredConstructors(); try { cons[0].setAccessible(true); // 同 Class#`newInstance()` 方法的功能,它也可創建無參建構函數 Constructor_2 instance = (Constructor_2) cons[0].newInstance(); instance.print(); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } } ``` > ![image](https://hackmd.io/_uploads/BJAvw_DdR.png) * **`Constructor` 可實例化有參、私有建構函數**,可透過 `new Object[] {}` 物件傳入參數 > `public T newInstance(Object... initargs)` ```java= Class<?> c2 = Constructor_2.class; Constructor<?>[] cons = c2.getDeclaredConstructors(); try { cons[1].setAccessible(true); Constructor_2 instance = (Constructor_2) cons[1].newInstance(new Object[] {1}); instance.print(); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { e.printStackTrace(); } ``` > ![image](https://hackmd.io/_uploads/BybAPdvdC.png) ### 取得類「所有 public」建構函數 * 以下範例為使用 `getConstructors()` 方法取得 Constructor 物件並使用,它的特點在於 ^1.^ 只取得 `public` 建構函數、^2.^ 也同時可以取得所有父類 `public` 建構函數 範例如下: 1. **目標類**:在這裡我們設計 `Constructor_3` 類,並且開類擁有三種不同訪問權的建構函數(分別是 `package`、`public`、`private` 三種訪問權),用來觀察 `getConstructors()` 函數可取得的建構函數 ```java= class Constructor_3 { Constructor_3() { System.out.println("執行 Constructor_3 建構無參函數"); } public Constructor_3(String str) { System.out.println("執行 Constructor_3 建構函數, str = " + str); } private Constructor_3(int a) { System.out.println("執行 Constructor_3 建構函數, a = " + a); } } ``` 2. **反射目標類**: 我們觀察是否都是取得 `public` 訪問權的建構函數(這裡我們觀察數量) ```java= public static void main(String[] args) { Class<Constructor_3> c3 = Constructor_3.class; Constructor<?>[] cons = c3.getConstructors(); System.out.println("Public construct count: " + cons.length); } ``` 如下圖中我們可以看到取得的建構類(`Constructor`)確實只有一個 > ![image](https://hackmd.io/_uploads/HkX2kKwuC.png) ### 取得「指定」建構函數 * 在上面小節的案例中,我們都是一次性獲取所有的建構函數(`getConstructors()`、`getDeclaredConstructors()` 函數),但其實我們透過 `Class 陣列` 來指定參數類型,並獲取指定的建構函數 範例如下: 1. **目標類**:在這裡我們設計 `Constructor_4` 類,並設計不同的建構函數,並且每個建構函數有不同的參數(入參) ```java= class Constructor_4 { // 公開建構函數 public Constructor_4() { System.out.println("執行 Constructor_3 建構無參函數"); } // 私有建構函數 private Constructor_4(String str, int age) { System.out.println("執行 Constructor_3 建構函數, name = " + str + ", age = " + age); } } ``` 2. **反射目標類**: * 指定建構函數時(使用 `getDeclaredConstructor(...)` 方法)可以透過 `Class<?> 陣列` 來指定類的接收參數的類型 :::danger * 如果要訪問非 `public` 的建構函數時,需要透過 `setAccessible(true)` 讓該建構函數可訪問,否則會拋出 `IllegalAccessException` 異常 > ![image](https://hackmd.io/_uploads/SJ0X_cwdC.png) * **如果是基礎類就傳基礎類,而不是基礎類的包裝類** 也就是假設參數類型為 `int`,那就傳入 `int.class` 而不是 `Integer.class`(因為它們是不同的類) ::: * 透過 `Constructor` 建構物件時,也有要傳入對應的類型、順序的參數(**經由 `Object array` 呼叫指定建構函數**) ```java= public static void main(String[] args) { Class<Constructor_4> c4 = Constructor_4.class; Constructor<?> cons = null; try { cons = c4.getDeclaredConstructor(new Class[]{String.class, int.class}); // 設定私有建構函數可訪問! cons.setAccessible(true); cons.newInstance(new Object[] {"Alien", 24}); cons = c4.getDeclaredConstructor(); cons.newInstance(); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | InvocationTargetException e1) { e1.printStackTrace(); } } ``` > ![image](https://hackmd.io/_uploads/BkcqF5PdA.png) ## 類的方法 [Method](https://developer.android.com/reference/java/lang/reflect/Method) 前面我們有介紹到 Class 類中有保存方法資訊(Method information),在這裡我們就可以透過 Class 類來取得方法資訊 > 「**透過 `Method` 物件來表示類的方法**」 ### 取得類的「全部」Method * 從 Class 類中可以取得類中的方法資訊,一般來講我們可以透過以下方法取得(如下表) | Class 類取得類的方法 | 訪問範圍 | | ----------------------------- | ---------------------------------------------------------------- | | `getMethods()` | **限定** 物件 `public` 方法 ==**包含父類方法**== | | `getDeclaredMethods()` | 物件**全部**方法包括 static and private 方法,==**限定該類**== | 使用範例如下: 1. **目標類**:在這個類中,我們設計 ^1.^ `BaseMethod` 作為父類方法並且其中有 `public`、`protected`、`public` 方法,^2.^ 另外讓 `MyMethod` 繼承 `BaseMethod` 方法,觀察每個反射方法涉及的範圍 ```java= class BaseMethod { void packageFunc() { System.out.println("Package function"); } protected void protectedFunc() { System.out.println("Protected function"); } public void publicFunc() { System.out.println("Public function"); } } class MyMethod extends BaseMethod { private int a = 0; private static int b = 0; public void print() { System.out.println("a is " + a + ", b is " + b); } public void setA(int a) { this.a = a; } private static void setB(int b) { MyMethod.b = b; } } ``` 2. **反射目標類的方法**: * 透過 Class 類的 `getMethods()` 方法取得目標類的所有 `public` 方法,其中也包括「父類」的 `public` 方法 ```java= public static void main(String[] args) { MyMethod my = new MyMethod(); Class<?> clz = my.getClass(); Method[] ms = clz.getMethods(); System.out.println("clz.getMethods(): " + ms.length); for(Method m : ms) { System.out.println("Method name: " + m.getName()); } ``` 從下圖中,我們也可以觀察到,除了目標類的 `public` 父類的方法的確就只能讀取到 `public` 方法(`publicFunc`) > ![image](https://hackmd.io/_uploads/rkwfRsvuA.png) * 透過 Class 類的 `getDeclaredMethods()` 方法取得目標類的「所有方法」,不包括父類方法 ```java= public static void main(String[] args) { MyMethod my = new MyMethod(); Class<?> clz = my.getClass(); Method[] dms = clz.getDeclaredMethods(); System.out.println("clz.getDeclaredMethods(): " + dms.length); for(Method m : dms) { System.out.println("Method name: " + m.getName()); } } ``` 下圖中,我們可以看到它會取得所有的方法(**包括靜態、私有的方法**) > ![image](https://hackmd.io/_uploads/BkeqynwuR.png) ### 取得類的「指定」Method * 同樣的,我們也可以透過 Class 類取得指定的方法的 Method 物件,如下表所示 | Class類取得類的方法 | 參數解釋 | | ------------------------------------------- | ------------------------------------------------------------------------------------- | | `getMethod(String, Class...<\>)` | 指定方法名稱,Class 為引數的類,==**訪問限定**物件的 public 方法 (包含父類)== | | `getDeclaredMethod(String, Class...<?>)` | 同上,但方法包括當前類的所有方法(限定當前類,不包括父類) | 範例如下: 1. **目標類**: ```java= class BaseMethod { void packageFunc() { System.out.println("Package function"); } protected void protectedFunc() { System.out.println("Protected function"); } public void publicFunc() { System.out.println("Public function"); } } class MyMethod extends BaseMethod { private int a = 0; private static int b = 0; public void print() { System.out.println("a is " + a + ", b is " + b); } public void setA(int a) { this.a = a; } private static void setB(int b) { MyMethod.b = b; } } ``` 2. **反射目標類的方法**: 以下我們透過 `getMethod`、`getDeclaredMethod` 方法指定方法名來取得 Method 物件(說明請看註解) ```java= public static void main(String[] args) { MyMethod my = new MyMethod(); Class<?> clz = my.getClass(); try { // 取得 public 父類方法 Method ms = clz.getMethod("publicFunc"); System.out.println("Parent Method name: " + ms.getName()); // 取得自己的 public 方法 Method ms2 = clz.getMethod("print"); System.out.println("Method name: " + ms2.getName()); // 取得自己的方法 Method dms = clz.getDeclaredMethod("setA", new Class[] {int.class}); System.out.println("Declared Method name: " + dms.getName()); // 取得自己的靜態、私有方法 Method dms2 = clz.getDeclaredMethod("setB", new Class[] {int.class}); System.out.println("Declared static Method name: " + dms2.getName()); } catch (Exception e) { e.printStackTrace(); } } ``` > ![image](https://hackmd.io/_uploads/Hy3Y8hPuR.png) ### 透過 Method 呼叫方法 * 當我們取得 Method 物件後,我們就可以透過 Method#`invoke(...)` 來呼叫該方法,如下表所示 | Class類取得類的方法 | 參數解釋 | | ------------------------------------------- | ------------------------------------------------------------------------------------- | | `invoke(Object obj, Object... args)` | **第一個 `Object` 是目標物件的實例**,之後的參數則是呼叫該方法時要傳入的參數 | 1. **目標類**: ```java= class BaseMethod { void packageFunc() { System.out.println("Package function"); } protected void protectedFunc() { System.out.println("Protected function"); } public void publicFunc() { System.out.println("Public function"); } } class MyMethod extends BaseMethod { private int a = 0; private static int b = 0; public void print() { System.out.println("a is " + a + ", b is " + b); } public void setA(int a) { this.a = a; } private static void setB(int b) { MyMethod.b = b; } } ``` 2. **反射目標類**: :::warning * 在使用 `invoke(...)` 調用原來類的方法時,第一個參數需要是目標物件的實例,並且如果方法並非是 `public` 方法,那就需要使用 `setAccessible(true)` 方法,把該方法設定為可訪問 > 並免出現 `IllegalAccessException` 異常 ::: ```java= public static void main(String[] args) { MyMethod my = new MyMethod(); Class<?> clz = my.getClass(); try { Method ms = clz.getMethod("publicFunc"); ms.setAccessible(true); ms.invoke(my); Method ms2 = clz.getMethod("print"); ms2.setAccessible(true); ms2.invoke(my); Method dms = clz.getDeclaredMethod("setA", new Class[] {int.class}); dms.setAccessible(true); dms.invoke(my, 123); ms2.invoke(my); Method dms2 = clz.getDeclaredMethod("setB", new Class[] {int.class}); dms2.setAccessible(true); dms2.invoke(my, 666); ms2.invoke(my); } catch (Exception e) { e.printStackTrace(); } } ``` > ![image](https://hackmd.io/_uploads/r1xzhCw_C.png) ## 類的字段 [Field](https://developer.android.com/reference/java/lang/reflect/Field) 前面我們有介紹到 Class 類中會保存字段資訊(Field information),在這裡我們就可以透過 Class 類來取得字段資訊 > 「**透過 `Field` 物件來表示類的字段**」 ### 取得全部、指定字段 Field * 從 Class 類中,我們可以獲得指定類的 ==**字段(位置)**== | Class 類取得 Field 的方法 | 訪問範圍 | | ---------------------------- | ---------------------------------------------------------------- | | `getFields()` | **指定** 物件 `public` 字段(==**包含父類**==) | | `getField(String)` | 透過字段名稱,取得字段;**訪問指定**物件的 `public` 字段(==**包含父類**==) | | `getDeclaredFields()` | 物件 **全部字段**;包括 `static`、`private` 字段(==**限定該類**==) | | `getDeclaredField(String)` | 透過字段名稱,取得字段;全部字段包括 `static`、`private` 變數(==**限定該類**==) | 1. **目標類**: ```java= class MyMyField { public int aa = 0; } class MyField extends MyMyField { public int a = 10; private int b = 20; private static int c = 30; int getB() { return b; } } ``` 2. **反射目標類**: ```java= public static void main(String[] args) { MyField m = new MyField(); Class<?> clz = m.getClass(); // 取得全部 public 字段(包括父類) Field[] fs = clz.getFields(); System.out.println("clz.getFields: " + fs.length); for(Field f : fs) { System.out.println("Field Name: " + f.getName()); } // 取得全部 public 字段(只限定自身類) Field[] dfs = clz.getDeclaredFields(); System.out.println("clz.getDeclaredFields: " + dfs.length); for(Field f : dfs) { System.out.println("Field Name: " + f.getName()); } } ``` > ![](https://i.imgur.com/teI6tNB.png) ### 「取得」字段的實例 * 在取得 Field 字段(物件)後,就可以透過 Field#`get(Object)` 方法就可以取得該字段的實例(也就是 取得==真正變數==) | Field 的方法 | 解釋 | | ---------------- | ---------------------------------- | | `get(Object)` | 透過物件,取得變數,**++必須強轉型++** | | `getInt(Object`) | 同上但不必強轉型,自動轉為 int | | `getXXX(Object)` | XXX 為基礎型態 | 1. **目標類**: ```java= class MyField { public int a = 10; private int b = 20; private static int c = 30; int getB() { return b; } int getC() { return c; } } ``` 2. **反射目標類**:(請看註解說明) ```java= public static void main(String[] args) { MyField m = new MyField(); Class<?> clz = m.getClass(); try { // 訪問物件 public 變數 Field fa = MyField.class.getDeclaredField("a"); System.out.println("Instances, access Field a: " + fa.get(m)); // 訪問物件 private 變數要多設置可訪問 setAccessible(true) Field fb = clz.getDeclaredField("b"); fb.setAccessible(true); System.out.println("Instances, access Field a: " + fb.getInt(m)); // 也可以訪問 static 變數 Field fc = clz.getDeclaredField("c"); fc.setAccessible(true); System.out.println("Instances, access Field a: " + fc.getInt(m)); } catch (Exception e) { e.printStackTrace(); } } ``` :::success * **當未實例化時,透過 Class 類也可以訪問物件的變數,但是只能訪問靜態變數 (static params)**,也就是 Object 包括 class ```java= public static void main(String[] args) { MyField m = new MyField(); Class<?> clz = m.getClass(); try { Field fc = MyField.class.getDeclaredField("c"); fc.setAccessible(true); System.out.println("No Instances, get static Field c: " + fc.getInt(MyField.class)); } catch (Exception e) { e.printStackTrace(); System.out.println("Cannot access non instances object"); } } ``` ::: > ![](https://i.imgur.com/tlQ0mmm.png) ### 「設定」字段的實例 * 在取得 Field 字段(物件)後,就可以透過 Field#`set(Object, ...)` 方法就可以設定該字段的實例(也就是 設定==變數進實例==) | Field 的方法 | 解釋 | | --------------------- | ------------------ | | `set(Object, value)` | 透過物件,設定變數 | | `setInt(Object, value)` | 同上 | | `setXXX(Object, value)` | XXX 為基礎型態 | 1. **目標類**: ```java= class MyField { public int a = 10; private int b = 20; private static int c = 30; int getB() { return b; } int getC() { return c; } } ``` 2. **反射目標類**:(請看註解說明) ```java= public static void main(String[] args) { MyField m = new MyField(); Class<?> clz = m.getClass(); try { // 兩個參數一個物件、一個是要設定的值 Field fa = MyField.class.getDeclaredField("a"); System.out.println("Instances, access Field a: " + fa.get(m)); fa.set(m, 111); System.out.println("after change a: " + m.a); // 當要設定 private 參數時要先設定可訪問,setAccessible(true) Field fb = clz.getDeclaredField("b"); fb.setAccessible(true); System.out.println("Instances, access Field a: " + fb.getInt(m)); fb.setInt(m, 222); System.out.println("after change b: " + m.getB()); // 同樣的,可以設定 static 變數 Field fc = clz.getDeclaredField("c"); fc.setAccessible(true); System.out.println("Instances, access Field a: " + fc.getInt(m)); fc.setInt(m, 333); System.out.println("after change c: " + m.getC()); } catch (Exception e) { e.printStackTrace(); } } ``` :::success * static 變數其實也不用透過實例化才能設置參數,可直接透過 Class 類設定 ```java= public static void main(String[] args) { MyField m = new MyField(); Class<?> clz = m.getClass(); try { Field fcc = MyField.class.getDeclaredField("c"); fcc.setAccessible(true); System.out.println("No Instances, get static Field c: " + fcc.getInt(MyField.class)); fcc.setInt(MyField.class, 33333); System.out.println("after change c: " + fcc.getInt(MyField.class)); } catch (Exception e) { e.printStackTrace(); System.out.println("Cannot access non instances object"); } } ``` ::: > ![](https://i.imgur.com/EcTevGz.png) ### 反射創建數組 * 透過 `java.lang.reflect.Array` 包,可以使用 Java 實現的反射創建數組功能,範例如下:反射創建 String 數組空間 ```java= import java.lang.reflect.Array; public class ArrayUsage { public static void main(String[] args) { String[] myStr = (String[]) Array.newInstance(String.class, 10); for (String s : myStr) { System.out.println("String: " + s); } } } ``` 從下圖中,我們可以看到 Java 的確會創建數組空間,**但是不會設定每個元素的內容** > ![image](https://hackmd.io/_uploads/S1pz-gOu0.png) ## 反射註解 Annotation 反射註解是許多開源框架中會使用到的技巧之一,下面表格為常用於反射判斷註解的 Java Reflect API | 註解的反射 API 名 | 解釋 | | ----- | ------ | | `isAnnotation()` | 判斷類是否有註解 | | `isAnnotationPresent(Class<? extends Annotation>)` | 判斷 **類是否==應用了某個註解==**| | `getAnnotation(Class<A>)` | 返回 **註解物件** | | `getAnnotations()` | 由於一個類、參數上可以有多個註解,所以可以取得多個 Annotation(也就是返回 `Annotation[]`) | :::success 如果要使用註解反射技巧,那註解就要保留到運行期間!(`@Retention` 註解需設定為 `RUNTIME`) ::: :::info 如果不清楚「Java 註解」的話,可以點擊這篇連結去了解 [**深入探討 Android、Java 註解應用:分析註解與 Enum 的差異 | Android APT**](https://devtechascendancy.com/android-annotations-enums-guide/) ::: ### 反射 Class 類的註解:證明 Runtime 期間 * 反射 `Class` 類的註解,在這裡我們要證明「所有的註解要反射,都需要保留到 Runtime 期間才能反射」,範例如下 * **定義 Annotation 類**:這邊我們定義兩個特性的註解,一個保留到 `RUNTIME`,一個保留到 `CLASS` > 使用反射必須使用元註解(使用 `@Retention` 註解) ```java= // 保存到 RUNTIME @Retention(RetentionPolicy.RUNTIME) @interface ReAnTest_1 { int age() default 18; String name() default "Alien"; } // 保存到 CLASS @Retention(RetentionPolicy.CLASS) @interface ReAnTest_2 { int age() default 18; String name() default "Alien"; } ``` * **反射 Annotation 類**: ```java= import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @ReAnTest_1(age = 20, name = "Pan") @ReAnTest_2(age = 21, name = "Pana") public class reflectionAnnotation { public static void main(String[] args) { Class<?> clz = reflectionAnnotation.class; if(clz.isAnnotation()) { System.out.println("This class isAnnotation"); } if(clz.isAnnotationPresent(ReAnTest_2.class)) { System.out.println("This class Annotation by ReAnTest_2"); } else { System.out.println("This class Annotation \"not\" ReAnTest_2"); } if(clz.isAnnotationPresent(ReAnTest_1.class)) { System.out.println("This class Annotation by ReAnTest_1"); // getAnnotation(Class) 可以 **動態取得該類的註解物件**,並取得其值 ReAnTest_1 r = clz.getAnnotation(ReAnTest_1.class); System.out.println("Age: " + r.age()); System.out.println("Name: " + r.name()); } else { System.out.println("This class Annotation \"not\" ReAnTest_1"); } } } ``` 從下圖中我們可以看到,同樣都被註解,但是 **只有保存到 `RUNTIME`(ReAnTest_1 註解)的註解才能被反射偵測到,而 `CLASS`(`ReAnTest_2` 註解)則會被消除** > ![](https://i.imgur.com/g1DLUi6.png) ### 反射方法上的註解 * 如果要提取方法上的註解,首先就需要先透過 Class 類提取 Method 物件,再透過 Method 物件取得方法上的註解,範例如下: * **定義 Annotation 類**: ```java= @Retention(RetentionPolicy.RUNTIME) @interface ReAnTest_1 { int age() default 18; String name() default "Alien"; } ``` * **將 `ReAnTest_1` 註解用類中的方法上**: ```java= class MyAnnotationClass { private int age = 10; private String name = "kyle"; @ReAnTest_1(age = 20, name = "Pan") void MyFunction() { System.out.println("age : " + age); System.out.println("name : " + name); } } ``` * **使用反射取得方法上註解的內容**:透過 Class 類取得 Method,並且透過 `getAnnotation(...)` 方法取得指定註解以及資訊 ```java= public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, NoSuchFieldException { MyAnnotationClass m = new MyAnnotationClass(); Class<?> clz = m.getClass(); Method method = clz.getDeclaredMethod("MyFunction"); if(method.isAnnotationPresent(ReAnTest_1.class)) { Field AGE = clz.getDeclaredField("age"); Field NAME = clz.getDeclaredField("name"); AGE.setAccessible(true); NAME.setAccessible(true); int age = AGE.getInt(m); String name = (String) NAME.get(m); System.out.println("Original age: " + age + ", name: " + name); ReAnTest_1 r = method.getAnnotation(ReAnTest_1.class); AGE.setInt(m, r.age()); NAME.set(m, r.name()); System.out.println("Change it by Annotation"); method.invoke(m); } else { method.invoke(m); } } ``` > ![](https://i.imgur.com/Xo3ovRD.png) ### 反射 Field 註解 * 同樣的,註解也可以使用類的字段上 如果要提取字段上的註解,首先就需要先透過 Class 類提取 Field 物件,再透過 Field 物件取得字段上的註解,範例如下: > 以下用 Android 來測試,使用反射來做 `findViewById()` 的行為,並做出設定文字 * **定義 Annotation 類**: ```java= // 註解 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface MyAnnotation { @IdRes int id() default -1; } ``` * **使用 `@MyAnnotation` 註解在類的字段上** ```java= public class MainActivity extends AppCompatActivity { @MyAnnotation(id = R.id.sample_text) TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MyClass.Inject_by_reflection(this); tv.setText("Yeah 123"); } } ``` * **使用反射取得字段上註解的內容**:取得內容後(這個內容就是 Layout ID),就可以使用它來取得對應的 View,並且設定其設定到註解的參數上! ```java= import android.app.Activity; import android.view.View; import java.lang.reflect.Field; public class MyClass { public static void Inject_by_reflection(Activity activity) { Class<? extends Activity> clz = activity.getClass(); Field[] fields = clz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); // Retention Runtime !! MyAnnotation anno = field.getAnnotation(MyAnnotation.class); if(anno == null) { return; } int id = anno.id(); if (id != -1) { View view = activity.findViewById(id); try { field.set(activity, view); // (物件,設定已更改的內容) } catch (IllegalAccessException e) { e.printStackTrace(); } } } } } ``` ## 反射泛型 Generic **當對於一個泛型類進行反射時,需要透過 Type 體系**,該體系由 5 個界面組成(`Type` 為基礎界面),如下表所示 | 對於泛型,反射提供的界面 | 功能 | Function | | -------- | -------- | -------- | | Type | 泛型名稱 | getTypeName | | TypeVariable | **泛型細節**,泛型名稱、全類名,可搜尋到++上限++的類 | getName \ getGenericDeclaration \ getBounds | | ParameterizedType | **泛型類**,返回具體的類型類型,可取得原數據中泛型簽名類型 (泛型真實類型) | getActualTypeArguments \ getRawType \ getOwnerType | | GenericArrayType | 參數類型為 **泛型數組** 時(List[]\Map[]) | getGenericComponentType | | WildcardType | **通配符**,獲取通配符泛型++上下限++ | getUpperBounds \ getLowerBounds | :::info * 這些繼承 `Type` 界面的子界面 **分別實現了++不同泛型對應的參數++** > ![](https://i.imgur.com/nkyjNH8.png) ::: ### getGenericType 取得泛型資訊:TypeVariable 捕捉編階 * 透過 Field#`getTypeName`、`getGenericType()` 方法可以取得保留在 Class 類中字段的「泛型資訊」 1. Type#`getTypeName()`、TypeVariable#`getName()` 方法取得的是一個泛型符號,而不是真正的類 2. 透過 TypeVariable#`getBounds()` 方法可以取得泛型的界線數組,之所以是數組是因為泛型可以有多個限制 :::info 這個範例中,我們來抓取泛型 [**限定類型 Qulified Type**](https://devtechascendancy.com/java-generics-complete-guide/#%E9%99%90%E5%AE%9A%E9%A1%9E%E5%9E%8B_Qulified_Type) 的邊界,該範例規範上界至少要實作 `Cloneable` 界面 ::: * 如果有明確的上界則返回上界的類型 * 沒有明確的上界則是返回 Object 類型 ```java= class MyBookMark implements Cloneable{} // 邊界為 Cloneable public class TestType<K extends Cloneable, V> { K key; V value; public static void main(String[] args) throws NoSuchFieldException, SecurityException { TestType<MyBookMark, Integer> book = new TestType<>(); Field fk = book.getClass().getDeclaredField("key"); // K Field fv = book.getClass().getDeclaredField("value"); // V TypeVariable<?> keyType = (TypeVariable<?>) fk.getGenericType(); TypeVariable<?> valueType = (TypeVariable<?>) fv.getGenericType(); // Type 界面的 getTypeName 方法 System.out.println("Type's getTypeName: " + keyType.getTypeName()); System.out.println("Type's getTypeName: " + valueType.getTypeName() + "\n"); // TypeVariable 界面的 getName 方法 System.out.println("TypeVariable's getName: " + keyType.getName()); System.out.println("TypeVariable's getName: " + valueType.getName() + "\n"); // TypeVariable 界面的 getGenericDeclaration 方法 System.out.println("TypeVariable's getGenericDeclaration: " + keyType.getGenericDeclaration()); System.out.println("TypeVariable's getGenericDeclaration: " + valueType.getGenericDeclaration() + "\n"); // TypeVariable 界面的 getBounds 方法,返回數組 for(Type t : keyType.getBounds()) { //"2. " System.out.println("TypeVariable's getBounds: " + t.toString()); } for(Type t : valueType.getBounds()) { System.out.println("TypeVariable's getBounds: " + t.toString()); } } } ``` 從下圖來看,我們也確實可以看到透過反射界面 `TypeVariable` 提供的方法,確實可以捕捉到泛型的邊界 > ![](https://i.imgur.com/cWvEhLK.png) ### ParameterizedType 具體類型 * 反射透過 `ParameterizedType` 界面提供的功能,我們可以捕捉擁有泛型參數的具體類型(透過 `getActualTypeArguments()` 方法) :::info 它與 `TypeVariable` 不同,它只需要透過指定泛型類型,就可以取得該泛型類型的確切泛型資訊 ::: 範例如下 ```java= public class TestParamType { Map<String, Integer> map; public static void main(String[] args) throws NoSuchFieldException, SecurityException { Field f = TestParamType.class.getDeclaredField("map"); ParameterizedType pType = (ParameterizedType) f.getGenericType(); System.out.println("ParameterizedType: " + pType); System.out.println("getRawType: " + pType.getRawType()); // 返回代表的 class System.out.println("getOwnerType: " + pType.getOwnerType()); for(Type t : pType.getActualTypeArguments()) { // 獲得具體類型 System.out.println("getActualTypeArguments: " + t); } } } ``` > ![](https://i.imgur.com/3sNSi3G.png) ### GenericArrayType 泛型數組 * 對於 `List` 相關類型的泛型,可以使用反射的 `GenericArrayType` 界面捕捉泛型資訊 範例如下: ```java= import java.lang.reflect.*; import java.util.List; public class ArrayType { List<String>[] lists; public static void main(String[] args) throws NoSuchFieldException, SecurityException { Field ff = ArrayType.class.getDeclaredField("lists"); GenericArrayType genericType = (GenericArrayType) ff.getGenericType(); System.out.println("getGenericComponentType: " + genericType.getGenericComponentType()); } } ``` > ![](https://i.imgur.com/aTsNbuB.png) ### WildcardType 通配符 :::info 如果對泛型的「[**通配符**](https://devtechascendancy.com/java-generics-complete-guide/#%E9%80%9A%E9%85%8D%E7%AC%A6%E5%8A%9F%E8%83%BD%EF%BC%9A%E6%B3%9B%E5%9E%8B%E9%A1%9E%E4%B9%8B%E9%96%93%E7%9A%84%E9%97%9C%E4%BF%82)」不了解,可以先點擊連結 ::: * 要獲取通配符的上下限,需要先透過 `ParameterizedType` 取得泛型類的具體類型,透過再它取得通配符的資訊(使用 `getActualTypeArguments()` 方法) ```java= import java.lang.reflect.*; import java.util.List; public class WildType { List<? extends Number> a; // 上界 List<? super String> b; // 下界 public static void main(String[] args) throws NoSuchFieldException, SecurityException { Field fa = WildType.class.getDeclaredField("a"); Field fb = WildType.class.getDeclaredField("b"); // 1. 先獲取泛型實體 ParameterizedType pa = (ParameterizedType) fa.getGenericType(); ParameterizedType pb = (ParameterizedType) fb.getGenericType(); System.out.println("pa: " + pa.getTypeName() + ", getRawType" + pa.getRawType()); System.out.println("pb: " + pb.getTypeName() + ", getRawType" + pb.getRawType()); // 2. 從泛型中拿到通配符 WildcardType wTypeA = (WildcardType) pa.getActualTypeArguments()[0]; // 可能有多個上下限 WildcardType wTypeB = (WildcardType) pb.getActualTypeArguments()[0]; System.out.println(wTypeA.getUpperBounds()[0]);// 可能有多個上下限 System.out.println(wTypeB.getLowerBounds()[0]);// 可能有多個上下限 } } ``` > ![](https://i.imgur.com/bXMqgx7.png) ## Appendix & FAQ :::info ::: ###### tags: `Java 基礎進階` `反射`