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
● .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 | 類的額外屬性,包括類層次結構、源文件名稱、註解、調試信息等等 |
● 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 關鍵字來獲得實例
透過 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 類,並且開類擁有三種不同訪問權的建構函數(分別是 package、public、private 三種訪問權),用來觀察 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 作為父類方法並且其中有 public、protected、public 方法,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. 反射目標類的方法:
以下我們透過 getMethod、getDeclaredMethod 方法指定方法名來取得 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();
}
}
透過 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() | 物件 全部字段;包括 static、private 字段(限定該類) |
getDeclaredField(String) | 透過字段名稱,取得字段;全部字段包括 static、private 變數(限定該類) |
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 註解)的註解才能被反射偵測到,而 CLASS(ReAnTest_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#getTypeName、getGenericType() 方法可以取得保留在 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 應用與編譯:從原始檔到命令產出 JavaDoc 文件 | JDK 結構
● 深入理解 Java 運算子與修飾符 | 重要概念、細節 | equals 比較
● 深入探索 Java 物件創建與引用細節:Clone 和finalize 特性,以及強、軟、弱、虛引用
● 認識 Java 函數式編程:從 Lambda 表達式到方法引用 | 3 種方法引用
Java IO 相關文章
● 探索 Java IO 的奧秘,了解檔案操作、流處理、NIO等精彩內容!
深入 Java 多執行緒
● 這一系列文章將帶你深入了解 Java 多執行緒技術的各個方面,從基礎知識到進階應用,涵蓋了多執行緒編程的核心概念與實踐
深入 Java 物件導向
● 探索 Java 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!























