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 物件導向的奧妙,掌握介面、抽象類、繼承等重要概念!幫助你針對物件導向設計有更深入的了解!