kyle shanks
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    1
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    --- 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 基礎進階` `反射`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully