Try   HackMD

Java 反射、Meta Programming

Overview of Content

如有引用參考請詳註出處,感謝 :smile:

反射是運行 Running Time 的時候才會創建、尋找類,或是方法、建構子、屬性

反射是 Java 被視為動態語言的關鍵,在運行期借助 Reflection API 進行反射

在程序中一般的類是在編譯期間就確定 (泛型例外),而反射基置創建的物件是運行時才確定

認識 Class 類

JVM 加載 .class 檔案之後會在 JVM 中建立一個 Class 類,而 Class 類是指在「運行期間」的「物件」,這個物件內部會有類所有的描述(包括 Source Code、Annotation、Header、Static Field、Field … 等等資訊)

  • 為什麼要保留這些資訊?所有的語言都有嗎?

    不,並非所有的語言都會保留這些訊息(像 C 語言就沒有),像是更針對執行速度、在意應用大小的語言就不會保留這些資訊

    這些訊息我們會稱之為 MetaData,而針對 MetaData 進行操作的行為就稱之為「Meta Programming

    而保留這些訊息的程式語言(像是我們說的 Java)就可以更具有拓展性、自由度

class 檔案、Class 類差別?

  • 認識 .class 檔案

    我們在 IDE 中撰寫的檔案是 .java 檔案,而這個 .java 檔案無法直接在應用中執行(JVM 無法直接加載 .java 檔案)

    JVM 可運行的檔案是 .class 檔案,而要產出這個檔案就是要經過「編譯」的動作,透過編譯後就可以將 .java 檔案轉成 .class

    源碼編譯

    編譯

    .java 檔案

    .class 檔案

    • .class 檔案中的資訊

      .class 文件是 Java 編譯器生成的二進制文件,包含了 JVM(Java 虛擬機)可以直接解讀和執行的字節碼… class 文件中包含以下幾個主要部分的資訊:

      class 檔案中的資訊 概述
      Magic Number 用於標識這是一個 Java 類文件,固定為 0xCAFEBABE
      Version Info Java 類文件的版本號,包括次版本號、主版本號
      Constant Pool 包含類文件中用到的所有常量,包括字符串、類名、方法名、字段名… 等等;常量池在類文件中佔據了很大的一部分
      Access Flags 用於標識類或接口的訪問權限和屬性,例如這個類是否是 publicfinalabstract
      This Class 當前類的名稱
      Super Class 這個類的超類(父類)的名稱,如果這個類是 java.lang.Object,則超類為空
      Interfaces 這個類實現的所有介面
      Fields 類中定義的所有字段的資訊,包括名稱、類型和訪問修飾符
      Methods 類中定義的所有方法的資訊,包括方法名、返回類型、參數列表、訪問修飾符和方法的字節碼
      Attributes 類的額外屬性,包括類層次結構、源文件名稱、註解、調試信息等等

      .class 內容 2

      Super Class

      Interfaces

      Fields

      Methods

      Attributes

      .class 內容 1

      Magic Number

      Version Info

      Constant Pool

      Access Flags

      This Class

  • Class 類:請注意這個「類」這個關鍵字,這個類就是一個物件,這個物件會保存在 JVM 的方法區;

    而 Class 類就是封裝了 .class 檔案中所對應的類的訊息,方便我們在運行期間可以讀取這些資訊

    • 更多有關 JVM 與類加載 的概念請點擊連結去深入了解

      在這邊我們可以簡單地去認知,一個 Class 類在 JVM 中只會擁有一個實例(instance

      Runtime

      類加載, Class 類

      實例化 class

      .class 檔案

      JVM

      物件

取得 Class 類

  • 這裡先說明如何透過程式取得 Class 類,之後章節再說明取得 Class 類之後可以做些什麼事… 我們可以透過以下三種方式來取得 Class 類,範例如下:

    1. 透過指定「類名」直接取得指定的 Class 類

      ​​​​​​​​public static void main(String[] args) { ​​​​​​​​ ​​​​​​​​ System.out.println( ​​​​​​​​ "Thread.class.toString: " + Thread.class.toString() ​​​​​​​​ ); ​​​​​​​​ ​​​​​​​​}
    2. 透過 Class#forName 方法,並輸入完整的類路徑來取得 Class 類

      ​​​​​​​​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 類

      ​​​​​​​​public static void main(String[] args) { ​​​​​​​​ ​​​​​​​​ Thread t = new Thread(); ​​​​​​​​ Class<?> clzz = t.getClass(); ​​​​​​​​ ​​​​​​​​ System.out.println("Thread.getClass: " + clzz.toString()); ​​​​​​​​ ​​​​​​​​}

      Image Not Showing Possible Reasons
      • The image file may be corrupted
      • The server hosting the image is unavailable
      • The image path is incorrect
      • The image format is not supported
      Learn More →

認識 Java 反射包

  • Java 有提供一個標準包用來分析 MetaData 並使用,該包在 java.lang.reflect 中,該包提供了一組用於反射(Reflection)操作的類和界面

    簡單來說反射就是:是一種允許程序在運行時檢查和修改其自身結構的功能

    這些類和界面使得程序可以動態地獲取類的結構信息(如類名、方法、字段、構造函數等),並且可以在運行時調用方法、訪問字段和創建實例

    • 反射的主要類

      主要類 概述
      Constructor 代表類的建構函數,提供方法來創建新實例,包括傳遞參數
      Method 提供方法來調用方法,包括傳遞參數、獲取返回值
      Field 提供方法來讀取和設置字段的值,無論字段是私有、保護還是公共的
      Array 提供靜態方法來動態創建和操作數組
    • 反射的主要界面

      主要類 概述
      Type 所有類型的公共界面
      InvocationHandler 用於處理代理實例上的方法調用
      GenericArrayType(用來處理泛型) 代表泛型數組類型
      ParameterizedType(用來處理泛型) 代表參數化類型
      TypeVariable(用來處理泛型) 代表類型變量,是泛型中的一部分
      WildcardType(用來處理泛型) 代表通配符類型,是泛型中的一部分

反射的注意事項

  • 反射會消耗一定的系統資源,多少會影響應用效能

  • 反射調用可 忽略權限檢查,可能會破壞封裝導置安全問題

  • 另外,對於可設定混淆的應用(像是 Android App 應用),就要特別小心!有使用反射技巧的程式,要記得設定跳過混淆,否則會造成框架無法正常運行!!

    因為很多框架會依賴反射機制,而混淆會導致 .class 類中保存的訊息與我們認知的訊息不同

類的建構器 Constructor

前面我們有介紹到 Class 類中有保存建構器(也就是構造函數的資訊),在這裡我們就可以透過建構器來實例化類別,而不用透過 new 關鍵字來獲得實例

使用

使用

實例化

new 關鍵字

MetaData 中的 Constructor

透過 Class 實例化:newInstance

  • 透過 Class#newInstance() 方法可以直接創建一個 無參的建構函數的實例

    範例如下:

    1. 目標類:目標類為一個無參數的 public 建構函數

      ​​​​​​​​class Constructor_1 { ​​​​​​​​ public Constructor_1() { ​​​​​​​​ System.out.println("執行 Constructor_1 建構函數"); ​​​​​​​​ } ​​​​​​​​ ​​​​​​​​ void print() { ​​​​​​​​ System.out.println("Hello World"); ​​​​​​​​ } ​​​​​​​​}
    2. 反射目標類:透過 Class#newInstance 方法直接處實例化物件,實例化後就可以以一般操作物件的方式使用

      ​​​​​​​​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(); ​​​​​​​​ } ​​​​​​​​}

      • 使用 newInstance 這種方式實例化物件時需要注意以下事項

        • 只能實例化無參建構函數,如果建構函數有參數,則會實例化失敗

        • 無法實例化使用 private 關鍵字描述的建構函數,否則會產生 IllegalAccessException 錯誤

取得類的 Constructor

  • 另外我們可以透過 Class 取得 Constructor 類,透過這個方法我們就可以取得 Class 類中對於建構函數所有的描述,並且可以透過它來實例化類別

    透過 Constructor 物件來表示類的建構函數

    Class 取得 Constructor 的方式 概述說明
    getConstructors() 取得所有公開的建構函數(不包括父類)
    getDeclaredConstructors() 取得所有建構函數,限定於該類(不包括父類)

取得類「自身全部」建構函數

  • 以下範例為使用 getDeclaredConstructors() 方法取得 Constructor 物件並使用

    範例如下:

    1. 目標類:該目標類中有兩個建構函數,一個是 public 的建構函數,以及另一個 private 描述的有參構造函數

      ​​​​​​​​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() 設定為可放問(非私有的可以不用設定)

        ​​​​​​​​​​​​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

      • Constructor 可實例化有參、私有建構函數,可透過 new Object[] {} 物件傳入參數

        public T newInstance(Object... initargs)

        ​​​​​​​​​​​​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

取得類「所有 public」建構函數

  • 以下範例為使用 getConstructors() 方法取得 Constructor 物件並使用,它的特點在於 1. 只取得 public 建構函數、2. 也同時可以取得所有父類 public 建構函數

    範例如下:

    1. 目標類:在這裡我們設計 Constructor_3 類,並且開類擁有三種不同訪問權的建構函數(分別是 packagepublicprivate 三種訪問權),用來觀察 getConstructors() 函數可取得的建構函數

      ​​​​​​​​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 訪問權的建構函數(這裡我們觀察數量)

      ​​​​​​​​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

取得「指定」建構函數

  • 在上面小節的案例中,我們都是一次性獲取所有的建構函數(getConstructors()getDeclaredConstructors() 函數),但其實我們透過 Class 陣列 來指定參數類型,並獲取指定的建構函數

    範例如下:

    1. 目標類:在這裡我們設計 Constructor_4 類,並設計不同的建構函數,並且每個建構函數有不同的參數(入參)

      ​​​​​​​​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<?> 陣列 來指定類的接收參數的類型

        • 如果要訪問非 public 的建構函數時,需要透過 setAccessible(true) 讓該建構函數可訪問,否則會拋出 IllegalAccessException 異常

          image

        • 如果是基礎類就傳基礎類,而不是基礎類的包裝類

          也就是假設參數類型為 int,那就傳入 int.class 而不是 Integer.class(因為它們是不同的類)

      • 透過 Constructor 建構物件時,也有要傳入對應的類型、順序的參數(經由 Object array 呼叫指定建構函數

        ​​​​​​​​​​​​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

類的方法 Method

前面我們有介紹到 Class 類中有保存方法資訊(Method information),在這裡我們就可以透過 Class 類來取得方法資訊

透過 Method 物件來表示類的方法

取得類的「全部」Method

  • 從 Class 類中可以取得類中的方法資訊,一般來講我們可以透過以下方法取得(如下表)

    Class 類取得類的方法 訪問範圍
    getMethods() 限定 物件 public 方法 包含父類方法
    getDeclaredMethods() 物件全部方法包括 static and private 方法,限定該類

    使用範例如下:

    1. 目標類:在這個類中,我們設計 1. BaseMethod 作為父類方法並且其中有 publicprotectedpublic 方法,2. 另外讓 MyMethod 繼承 BaseMethod 方法,觀察每個反射方法涉及的範圍

      ​​​​​​​​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 方法

        ​​​​​​​​​​​​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

      • 透過 Class 類的 getDeclaredMethods() 方法取得目標類的「所有方法」,不包括父類方法

        ​​​​​​​​​​​​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

取得類的「指定」Method

  • 同樣的,我們也可以透過 Class 類取得指定的方法的 Method 物件,如下表所示

    Class類取得類的方法 參數解釋
    getMethod(String, Class...<\>) 指定方法名稱,Class 為引數的類,訪問限定物件的 public 方法 (包含父類)
    getDeclaredMethod(String, Class...<?>) 同上,但方法包括當前類的所有方法(限定當前類,不包括父類)

    範例如下:

    1. 目標類

      ​​​​​​​​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. 反射目標類的方法

      以下我們透過 getMethodgetDeclaredMethod 方法指定方法名來取得 Method 物件(說明請看註解)

      ​​​​​​​​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

透過 Method 呼叫方法

  • 當我們取得 Method 物件後,我們就可以透過 Method#invoke(...) 來呼叫該方法,如下表所示

    Class類取得類的方法 參數解釋
    invoke(Object obj, Object... args) 第一個 Object 是目標物件的實例,之後的參數則是呼叫該方法時要傳入的參數
    1. 目標類

      ​​​​​​​​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. 反射目標類

      • 在使用 invoke(...) 調用原來類的方法時,第一個參數需要是目標物件的實例,並且如果方法並非是 public 方法,那就需要使用 setAccessible(true) 方法,把該方法設定為可訪問

        並免出現 IllegalAccessException 異常

      ​​​​​​​​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

類的字段 Field

前面我們有介紹到 Class 類中會保存字段資訊(Field information),在這裡我們就可以透過 Class 類來取得字段資訊

透過 Field 物件來表示類的字段

取得全部、指定字段 Field

  • 從 Class 類中,我們可以獲得指定類的 字段(位置)

    Class 類取得 Field 的方法 訪問範圍
    getFields() 指定 物件 public 字段(包含父類
    getField(String) 透過字段名稱,取得字段;訪問指定物件的 public 字段(包含父類
    getDeclaredFields() 物件 全部字段;包括 staticprivate 字段(限定該類
    getDeclaredField(String) 透過字段名稱,取得字段;全部字段包括 staticprivate 變數(限定該類
    1. 目標類

      ​​​​​​​​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. 反射目標類

      ​​​​​​​​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()); ​​​​​​​​ } ​​​​​​​​}

「取得」字段的實例

  • 在取得 Field 字段(物件)後,就可以透過 Field#get(Object) 方法就可以取得該字段的實例(也就是 取得真正變數

    Field 的方法 解釋
    get(Object) 透過物件,取得變數,必須強轉型
    getInt(Object) 同上但不必強轉型,自動轉為 int
    getXXX(Object) XXX 為基礎型態
    1. 目標類

      ​​​​​​​​class MyField { ​​​​​​​​ public int a = 10; ​​​​​​​​ private int b = 20; ​​​​​​​​ private static int c = 30; ​​​​​​​​ int getB() { ​​​​​​​​ return b; ​​​​​​​​ } ​​​​​​​​ int getC() { ​​​​​​​​ return c; ​​​​​​​​ } ​​​​​​​​}
    2. 反射目標類:(請看註解說明)

      ​​​​​​​​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(); ​​​​​​​​ } ​​​​​​​​ ​​​​​​​​}
      • 當未實例化時,透過 Class 類也可以訪問物件的變數,但是只能訪問靜態變數 (static params),也就是 Object 包括 class

        ​​​​​​​​​​​​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"); ​​​​​​​​​​​​ } ​​​​​​​​​​​​}

「設定」字段的實例

  • 在取得 Field 字段(物件)後,就可以透過 Field#set(Object, ...) 方法就可以設定該字段的實例(也就是 設定變數進實例

    Field 的方法 解釋
    set(Object, value) 透過物件,設定變數
    setInt(Object, value) 同上
    setXXX(Object, value) XXX 為基礎型態
    1. 目標類

      ​​​​​​​​class MyField { ​​​​​​​​ public int a = 10; ​​​​​​​​ private int b = 20; ​​​​​​​​ private static int c = 30; ​​​​​​​​ int getB() { ​​​​​​​​ return b; ​​​​​​​​ } ​​​​​​​​ int getC() { ​​​​​​​​ return c; ​​​​​​​​ } ​​​​​​​​}
    2. 反射目標類:(請看註解說明)

      ​​​​​​​​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(); ​​​​​​​​ } ​​​​​​​​}
      • static 變數其實也不用透過實例化才能設置參數,可直接透過 Class 類設定

        ​​​​​​​​​​​​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"); ​​​​​​​​​​​​ } ​​​​​​​​​​​​}

反射創建數組

  • 透過 java.lang.reflect.Array 包,可以使用 Java 實現的反射創建數組功能,範例如下:反射創建 String 數組空間

    ​​​​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

反射註解 Annotation

反射註解是許多開源框架中會使用到的技巧之一,下面表格為常用於反射判斷註解的 Java Reflect API

註解的反射 API 名 解釋
isAnnotation() 判斷類是否有註解
isAnnotationPresent(Class<? extends Annotation>) 判斷 類是否應用了某個註解
getAnnotation(Class<A>) 返回 註解物件
getAnnotations() 由於一個類、參數上可以有多個註解,所以可以取得多個 Annotation(也就是返回 Annotation[]

如果要使用註解反射技巧,那註解就要保留到運行期間!(@Retention 註解需設定為 RUNTIME

如果不清楚「Java 註解」的話,可以點擊這篇連結去了解 深入探討 Android、Java 註解應用:分析註解與 Enum 的差異 | Android APT

反射 Class 類的註解:證明 Runtime 期間

  • 反射 Class 類的註解,在這裡我們要證明「所有的註解要反射,都需要保留到 Runtime 期間才能反射」,範例如下

    • 定義 Annotation 類:這邊我們定義兩個特性的註解,一個保留到 RUNTIME,一個保留到 CLASS

      使用反射必須使用元註解(使用 @Retention 註解)

      ​​​​​​​​// 保存到 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 類

      ​​​​​​​​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 註解)的註解才能被反射偵測到,而 CLASSReAnTest_2 註解)則會被消除

反射方法上的註解

  • 如果要提取方法上的註解,首先就需要先透過 Class 類提取 Method 物件,再透過 Method 物件取得方法上的註解,範例如下:

    • 定義 Annotation 類

      ​​​​​​​​@Retention(RetentionPolicy.RUNTIME) ​​​​​​​​@interface ReAnTest_1 { ​​​​​​​​ int age() default 18; ​​​​​​​​ String name() default "Alien"; ​​​​​​​​}
    • ReAnTest_1 註解用類中的方法上

      ​​​​​​​​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(...) 方法取得指定註解以及資訊

      ​​​​​​​​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); ​​​​​​​​ } ​​​​​​​​}

反射 Field 註解

  • 同樣的,註解也可以使用類的字段上

    如果要提取字段上的註解,首先就需要先透過 Class 類提取 Field 物件,再透過 Field 物件取得字段上的註解,範例如下:

    以下用 Android 來測試,使用反射來做 findViewById() 的行為,並做出設定文字

    • 定義 Annotation 類

      ​​​​​​​​// 註解 ​​​​​​​​@Retention(RetentionPolicy.RUNTIME) ​​​​​​​​@Target(ElementType.FIELD) ​​​​​​​​public @interface MyAnnotation { ​​​​​​​​ @IdRes int id() default -1; ​​​​​​​​}
    • 使用 @MyAnnotation 註解在類的字段上

      ​​​​​​​​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,並且設定其設定到註解的參數上!

      ​​​​​​​​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
  • 這些繼承 Type 界面的子界面 分別實現了不同泛型對應的參數

getGenericType 取得泛型資訊:TypeVariable 捕捉編階

  • 透過 Field#getTypeNamegetGenericType() 方法可以取得保留在 Class 類中字段的「泛型資訊」

    1. Type#getTypeName()、TypeVariable#getName() 方法取得的是一個泛型符號,而不是真正的類

    2. 透過 TypeVariable#getBounds() 方法可以取得泛型的界線數組,之所以是數組是因為泛型可以有多個限制

      這個範例中,我們來抓取泛型 限定類型 Qulified Type 的邊界,該範例規範上界至少要實作 Cloneable 界面

      • 如果有明確的上界則返回上界的類型

      • 沒有明確的上界則是返回 Object 類型

      ​​​​​​​​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 提供的方法,確實可以捕捉到泛型的邊界

ParameterizedType 具體類型

  • 反射透過 ParameterizedType 界面提供的功能,我們可以捕捉擁有泛型參數的具體類型(透過 getActualTypeArguments() 方法)

    它與 TypeVariable 不同,它只需要透過指定泛型類型,就可以取得該泛型類型的確切泛型資訊

    範例如下

    ​​​​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); ​​​​ } ​​​​ } ​​​​}

GenericArrayType 泛型數組

  • 對於 List 相關類型的泛型,可以使用反射的 GenericArrayType 界面捕捉泛型資訊

    範例如下:

    ​​​​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()); ​​​​ } ​​​​}

WildcardType 通配符

如果對泛型的「通配符」不了解,可以先點擊連結

  • 要獲取通配符的上下限,需要先透過 ParameterizedType 取得泛型類的具體類型,透過再它取得通配符的資訊(使用 getActualTypeArguments() 方法)

    ​​​​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]);// 可能有多個上下限 ​​​​ } ​​​​}

Appendix & FAQ

tags: Java 基礎進階 反射