深入探索 Java 反射:理解並使用 Class 類 | Constructor、Method、Field、Annotation 和反射泛型

深入探索 Java 反射:理解並使用 Class 類 | Constructor、Method、Field、Annotation 和反射泛型

Overview of Content

在這篇文章中,我們將深入探討 Java 反射機制,並詳細解釋如何使用反射來操縱和訪問 Class 類、Constructor、Method、Field、Annotation 和泛型

首先,我們會介紹什麼是 Class 類以及如何取得和使用它們,接著,我們將深入了解 Java 反射包及其注意事項

然後,我們將詳細討論類的建構器(Constructor),包括如何透過 Class 實例化、取得類的 Constructor 以及如何獲取所有建構函數。隨後,我們會探索如何取得類的 Method 並通過反射呼叫方法… 接下來,我們會了解如何取得和操作類的字段(Field),包括如何反射創建數組

除此之外,我們還會探討反射註解(Annotation)的應用,並說明如何在運行期間使用反射來處理 Class 類、方法和字段上的註解

最後,我們將解釋反射泛型(Generic),並展示如何取得泛型資訊和處理具體類型、泛型數組和通配符。這篇文章將為您提供全面的 Java 反射知識,使您能夠更靈活地應用反射技術來提升您的 Java 開發技能。

寫文章分享不易,如有引用參考請詳註出處,如有指導、意見歡迎留言(如果覺得寫得好也請給我一些支持),感謝 😀

個人程式分享時比較注重「縮排」,所以可能不適合手機的排版閱讀,建議切換至「電腦版」、「平板版」視窗看


認識 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

graph LR subgraph 源碼編譯 j(.java 檔案) -.-> |編譯| c(.class 檔案) end

.class 檔案中的資訊

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

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

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

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

graph TB subgraph Runtime c(.class 檔案) --> |類加載, Class 類| JVM JVM --> |實例化 class| 物件 end

取得 Class 類

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

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


public static void main(String[] args) {
    
     System.out.println(
         "Thread.class.toString: " + Thread.class.toString()
     );
    
}

B. 透過 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();
    }
    
}

C. 透過實例物件(instance)的 getClass() 方法來取得 Class 類


public static void main(String[] args) {
    
    Thread t = new Thread();

    Class<?> clzz = t.getClass();
    
    System.out.println("Thread.getClass: " + clzz.toString());
    
}

認識 Java 反射包

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

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

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

反射的主要類

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

反射的主要界面

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

反射的注意事項

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

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

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

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


類的建構器 Constructor

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

graph LR 實例化 --> |使用| n(new 關鍵字) 實例化 --> |使用| r(MetaData 中的 Constructor)

透過 Class 實例化:newInstance

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

範例如下:

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


class Constructor_1 {

    public Constructor_1() {
        System.out.println("執行 Constructor_1 建構函數");
    }
    
    void print() {
        System.out.println("Hello World");
    }

}

B. 反射目標類:透過 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 物件並使用

範例如下:

A. 目標類:該目標類中有兩個建構函數,一個是 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");
    }
}

B. 反射目標類

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

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


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

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

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

範例如下:

A. 目標類:在這裡我們設計 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);
    }
}

B. 反射目標類

我們觀察是否都是取得 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)確實只有一個

取得「指定」建構函數

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

範例如下:

A. 目標類:在這裡我們設計 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);
    }

}

B. 反射目標類

● 指定建構函數時(使用 getDeclaredConstructor(...) 方法)可以透過 Class<?> 陣列 來指定類的接收參數的類型

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

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

也就是假設參數類型為 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();
    }
    
}

類的方法 Method

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

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

取得類的「全部」Method

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

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

使用範例如下:

A. 目標類:在這個類中,我們設計 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;
    }
}

B. 反射目標類的方法

● 透過 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

● 透過 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());
    }
}

下圖中,我們可以看到它會取得所有的方法(包括靜態、私有的方法

取得類的「指定」Method

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

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

範例如下:

A. 目標類


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

B. 反射目標類的方法

以下我們透過 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 是目標物件的實例,之後的參數則是呼叫該方法時要傳入的參數

A. 目標類


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

B. 反射目標類

● 在使用 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();
    }

}

類的字段 Field

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

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

取得全部、指定字段 Field

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

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

A. 目標類


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

B. 反射目標類


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 為基礎型態

A. 目標類


class MyField {
    public int a = 10;
    private int b = 20;
    private static int c = 30;

    int getB() {
        return b;
    }

    int getC() {
        return c;
    }
}

B. 反射目標類:(請看註解說明)


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 為基礎型態

A. 目標類


class MyField {
    public int a = 10;
    private int b = 20;
    private static int c = 30;

    int getB() {
        return b;
    }

    int getC() {
        return c;
    }
}

B. 反射目標類:(請看註解說明)


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 的確會創建數組空間,但是不會設定每個元素的內容


反射註解 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 為基礎界面),如下表所示

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

● 這些繼承 Type 界面的子界面 分別實現了不同泛型對應的參數

getGenericType 取得泛型資訊:TypeVariable 捕捉邊界

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

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

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

}


更多的 Java 語言相關文章

Java 語言深入

● 在這個系列中,我們全方位地探討了 Java 語言的各個核心主題,旨在幫助你徹底掌握這門強大的編程語言。無論你是想深入理解 Java 的基礎類型與變數作用域,還是探索異常處理與運算子的細節,這些文章都將為您提供寶貴的知識

深入 Java 物件導向

● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!


Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響