---
title: 'Java 反射、Meta Programming'
disqus: kyleAlien
---
Java 反射、Meta Programming
===
## Overview of Content
如有引用參考請詳註出處,感謝 :smile:
**反射是運行 `Running Time`** 的時候才會創建、尋找類,或是方法、建構子、屬性
> **反射是 Java 被視為動態語言的關鍵**,在運行期借助 Reflection API 進行反射
在程序中一般的類是在編譯期間就確定 (泛型例外),而反射基置創建的物件是運行時才確定
:::success
* 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
本篇文章對應的是 [**深入探索 Java 反射:理解並使用 Class 類 | Constructor、Method、Field、Annotation 和反射泛型**](https://devtechascendancy.com/java-reflection-guide_class-object/)
:::
[TOC]
## 認識 [Class 類](https://developer.android.com/reference/java/lang/Class)
JVM 加載 `.class` 檔案之後會在 JVM 中建立一個 `Class 類`,而 Class 類是指在「**運行期間**」的「**物件**」,這個物件內部會有類所有的描述(包括 Source Code、Annotation、Header、Static Field、Field … 等等資訊)
:::info
* **為什麼要保留這些資訊?所有的語言都有嗎?**
不,並非所有的語言都會保留這些訊息(像 C 語言就沒有),像是更針對執行速度、在意應用大小的語言就不會保留這些資訊
> 這些訊息我們會稱之為 MetaData,而針對 MetaData 進行操作的行為就稱之為「`Meta Programming`」
而保留這些訊息的程式語言(像是我們說的 Java)就可以更具有拓展性、自由度
:::
### class 檔案、Class 類差別?
* **認識 `.class` 檔案**:
我們在 IDE 中撰寫的檔案是 `.java` 檔案,而這個 `.java` 檔案無法直接在應用中執行(JVM 無法直接加載 `.java` 檔案)
JVM 可運行的檔案是 `.class` 檔案,而要產出這個檔案就是要經過「編譯」的動作,透過編譯後就可以將 `.java` 檔案轉成 `.class`
```mermaid
graph LR
subgraph 源碼編譯
j(.java 檔案) -.-> |編譯| c(.class 檔案)
end
```
* **`.class` 檔案中的資訊**
`.class` 文件是 Java 編譯器生成的二進制文件,包含了 JVM(Java 虛擬機)可以直接解讀和執行的字節碼… class 文件中包含以下幾個主要部分的資訊:
| class 檔案中的資訊 | 概述 |
| - | - |
| `Magic Number` | 用於標識這是一個 Java 類文件,固定為 `0xCAFEBABE` |
| `Version Info` | Java 類文件的版本號,包括次版本號、主版本號 |
| `Constant Pool` | 包含類文件中用到的所有常量,包括字符串、類名、方法名、字段名… 等等;常量池在類文件中佔據了很大的一部分 |
| `Access Flags` | 用於標識類或接口的訪問權限和屬性,例如這個類是否是 `public`、`final`、`abstract` 等 |
| `This Class` | 當前類的名稱 |
| `Super Class` | 這個類的超類(父類)的名稱,如果這個類是 `java.lang.Object`,則超類為空 |
| `Interfaces` | 這個類實現的所有介面 |
| `Fields` | 類中定義的所有字段的資訊,包括名稱、類型和訪問修飾符 |
| `Methods` | 類中定義的所有方法的資訊,包括方法名、返回類型、參數列表、訪問修飾符和方法的字節碼 |
| `Attributes` | 類的額外屬性,包括類層次結構、源文件名稱、註解、調試信息等等 |
```mermaid
graph LR
subgraph .class 內容 1
1(Magic Number)
2(Version Info)
3(Constant Pool)
4(Access Flags)
5(This Class)
end
subgraph .class 內容 2
6(Super Class)
7(Interfaces)
8(Fields)
9(Methods)
10(Attributes)
end
```
* `Class 類`:請注意這個「類」這個關鍵字,這個類就是一個物件,這個物件會保存在 JVM 的方法區;
而 Class 類就是封裝了 `.class` 檔案中所對應的類的訊息,方便我們在運行期間可以讀取這些資訊
:::success
* 更多有關 [**JVM 與類加載**](https://devtechascendancy.com/class-lifecycle_classloader-exploration_jvm/) 的概念請點擊連結去深入了解
在這邊我們可以簡單地去認知,一個 Class 類在 JVM 中只會擁有一個實例(`instance`)
```mermaid
graph TB
subgraph Runtime
c(.class 檔案) --> |類加載, Class 類| JVM
JVM --> |實例化 class| 物件
end
```
:::
### 取得 Class 類
* 這裡先說明如何透過程式取得 Class 類,之後章節再說明取得 Class 類之後可以做些什麼事… 我們可以透過以下三種方式來取得 `Class 類`,範例如下:
1. 透過指定「類名」直接取得指定的 Class 類
```java=
public static void main(String[] args) {
System.out.println(
"Thread.class.toString: " + Thread.class.toString()
);
}
```
2. 透過 Class#`forName` 方法,並輸入完整的類路徑來取得 Class 類
```java=
public static void main(String[] args) {
try {
Class<?> clz = Class.forName("java.lang.Thread");
System.out.println("Class.forName: " + clz.toString());
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
```
3. 透過實例物件(`instance`)的 `getClass()` 方法來取得 Class 類
```java=
public static void main(String[] args) {
Thread t = new Thread();
Class<?> clzz = t.getClass();
System.out.println("Thread.getClass: " + clzz.toString());
}
```
> ![](https://i.imgur.com/8EkFOjF.png)
### 認識 Java 反射包
* Java 有提供一個標準包用來分析 MetaData 並使用,該包在 [**`java.lang.reflect`**](https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/package-summary.html) 中,該包提供了一組用於反射(`Reflection`)操作的類和界面
> 簡單來說反射就是:是一種允許程序在運行時檢查和修改其自身結構的功能
這些類和界面使得程序可以動態地獲取類的結構信息(如類名、方法、字段、構造函數等),並且可以在運行時調用方法、訪問字段和創建實例
* **反射的主要類**
| 主要類 | 概述 |
| - | - |
| `Constructor` | 代表類的建構函數,提供方法來創建新實例,包括傳遞參數 |
| `Method` | 提供方法來調用方法,包括傳遞參數、獲取返回值 |
| `Field` | 提供方法來讀取和設置字段的值,無論字段是私有、保護還是公共的 |
| `Array` | 提供靜態方法來動態創建和操作數組 |
* **反射的主要界面**
| 主要類 | 概述 |
| - | - |
| `Type` | 所有類型的公共界面 |
| `InvocationHandler` | 用於處理代理實例上的方法調用 |
| `GenericArrayType`(用來處理泛型) | 代表泛型數組類型 |
| `ParameterizedType`(用來處理泛型) | 代表參數化類型 |
| `TypeVariable`(用來處理泛型) | 代表類型變量,是泛型中的一部分 |
| `WildcardType`(用來處理泛型) | 代表通配符類型,是泛型中的一部分 |
### 反射的注意事項
* 反射會消耗一定的系統資源,多少會影響應用效能
* 反射調用可 **忽略權限檢查**,可能會破壞封裝導置安全問題
* 另外,對於可設定混淆的應用(像是 Android App 應用),就要特別小心!有使用反射技巧的程式,要記得設定跳過混淆,否則會造成框架無法正常運行!!
:::danger
因為很多框架會依賴反射機制,而混淆會導致 `.class` 類中保存的訊息與我們認知的訊息不同
:::
## 類的建構器 [Constructor](https://developer.android.com/reference/java/lang/reflect/Constructor)
前面我們有介紹到 Class 類中有保存建構器(也就是構造函數的資訊),在這裡我們就可以透過建構器來實例化類別,而不用透過 `new` 關鍵字來獲得實例
```mermaid
graph LR
實例化 --> |使用| n(new 關鍵字)
實例化 --> |使用| r(MetaData 中的 Constructor)
```
### 透過 Class 實例化:newInstance
* 透過 Class#`newInstance()` 方法可以直接創建一個 **無參的建構函數的實例**
範例如下:
1. **目標類**:目標類為一個無參數的 `public` 建構函數
```java=
class Constructor_1 {
public Constructor_1() {
System.out.println("執行 Constructor_1 建構函數");
}
void print() {
System.out.println("Hello World");
}
}
```
2. **反射目標類**:透過 Class#`newInstance` 方法直接處實例化物件,實例化後就可以以一般操作物件的方式使用
```java=
public static void main(String[] args) {
Class<Constructor_1> c1 = Constructor_1.class;
try {
Constructor_1 instance = c1.newInstance();
instance.print();
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
```
> ![](https://i.imgur.com/CicQvXl.png)
:::warning
* **使用 `newInstance` 這種方式實例化物件時需要注意以下事項**
* **只能實例化無參建構函數**,如果建構函數有參數,則會實例化失敗
* **無法實例化使用 `private` 關鍵字描述的建構函數**,否則會產生 `IllegalAccessException` 錯誤
> ![](https://i.imgur.com/TB52Dh8.png)
:::
### 取得類的 Constructor
* 另外我們可以透過 Class 取得 `Constructor` 類,透過這個方法我們就可以取得 Class 類中對於建構函數所有的描述,並且可以透過它來實例化類別
> 「**透過 `Constructor` 物件來表示類的建構函數**」
| Class 取得 `Constructor` 的方式 | 概述說明 |
| - | - |
| `getConstructors()` | **取得所有公開的建構函數**(不包括父類) |
| `getDeclaredConstructors()` | **取得所有建構函數**,限定於該類(不包括父類) |
### 取得類「自身全部」建構函數
* 以下範例為使用 `getDeclaredConstructors()` 方法取得 Constructor 物件並使用
範例如下:
1. **目標類**:該目標類中有兩個建構函數,一個是 `public` 的建構函數,以及另一個 `private` 描述的有參構造函數
```java=
class Constructor_2 {
Constructor_2() {
System.out.println("執行 Constructor_2 建構函數");
}
private Constructor_2(int a) {
System.out.println("執行 Constructor_2 建構函數, a = " + a);
}
void print() {
System.out.println("Hello World");
}
}
```
2. **反射目標類**:
* Constructor 實例化類別比較特殊,**它可以訪問私有建構函數**,不過必須透過 setAccessible() 設定為可放問(非私有的可以不用設定)
```java=
public static void main(String[] args) {
Class<Constructor_2> c2 = Constructor_2.class;
Constructor<?>[] cons = c2.getDeclaredConstructors();
try {
cons[0].setAccessible(true);
// 同 Class#`newInstance()` 方法的功能,它也可創建無參建構函數
Constructor_2 instance = (Constructor_2) cons[0].newInstance();
instance.print();
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
e.printStackTrace();
}
}
```
> ![image](https://hackmd.io/_uploads/BJAvw_DdR.png)
* **`Constructor` 可實例化有參、私有建構函數**,可透過 `new Object[] {}` 物件傳入參數
> `public T newInstance(Object... initargs)`
```java=
Class<?> c2 = Constructor_2.class;
Constructor<?>[] cons = c2.getDeclaredConstructors();
try {
cons[1].setAccessible(true);
Constructor_2 instance = (Constructor_2) cons[1].newInstance(new Object[] {1});
instance.print();
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
e.printStackTrace();
}
```
> ![image](https://hackmd.io/_uploads/BybAPdvdC.png)
### 取得類「所有 public」建構函數
* 以下範例為使用 `getConstructors()` 方法取得 Constructor 物件並使用,它的特點在於 ^1.^ 只取得 `public` 建構函數、^2.^ 也同時可以取得所有父類 `public` 建構函數
範例如下:
1. **目標類**:在這裡我們設計 `Constructor_3` 類,並且開類擁有三種不同訪問權的建構函數(分別是 `package`、`public`、`private` 三種訪問權),用來觀察 `getConstructors()` 函數可取得的建構函數
```java=
class Constructor_3 {
Constructor_3() {
System.out.println("執行 Constructor_3 建構無參函數");
}
public Constructor_3(String str) {
System.out.println("執行 Constructor_3 建構函數, str = " + str);
}
private Constructor_3(int a) {
System.out.println("執行 Constructor_3 建構函數, a = " + a);
}
}
```
2. **反射目標類**:
我們觀察是否都是取得 `public` 訪問權的建構函數(這裡我們觀察數量)
```java=
public static void main(String[] args) {
Class<Constructor_3> c3 = Constructor_3.class;
Constructor<?>[] cons = c3.getConstructors();
System.out.println("Public construct count: " + cons.length);
}
```
如下圖中我們可以看到取得的建構類(`Constructor`)確實只有一個
> ![image](https://hackmd.io/_uploads/HkX2kKwuC.png)
### 取得「指定」建構函數
* 在上面小節的案例中,我們都是一次性獲取所有的建構函數(`getConstructors()`、`getDeclaredConstructors()` 函數),但其實我們透過 `Class 陣列` 來指定參數類型,並獲取指定的建構函數
範例如下:
1. **目標類**:在這裡我們設計 `Constructor_4` 類,並設計不同的建構函數,並且每個建構函數有不同的參數(入參)
```java=
class Constructor_4 {
// 公開建構函數
public Constructor_4() {
System.out.println("執行 Constructor_3 建構無參函數");
}
// 私有建構函數
private Constructor_4(String str, int age) {
System.out.println("執行 Constructor_3 建構函數, name = " + str + ", age = " + age);
}
}
```
2. **反射目標類**:
* 指定建構函數時(使用 `getDeclaredConstructor(...)` 方法)可以透過 `Class<?> 陣列` 來指定類的接收參數的類型
:::danger
* 如果要訪問非 `public` 的建構函數時,需要透過 `setAccessible(true)` 讓該建構函數可訪問,否則會拋出 `IllegalAccessException` 異常
> ![image](https://hackmd.io/_uploads/SJ0X_cwdC.png)
* **如果是基礎類就傳基礎類,而不是基礎類的包裝類**
也就是假設參數類型為 `int`,那就傳入 `int.class` 而不是 `Integer.class`(因為它們是不同的類)
:::
* 透過 `Constructor` 建構物件時,也有要傳入對應的類型、順序的參數(**經由 `Object array` 呼叫指定建構函數**)
```java=
public static void main(String[] args) {
Class<Constructor_4> c4 = Constructor_4.class;
Constructor<?> cons = null;
try {
cons = c4.getDeclaredConstructor(new Class[]{String.class, int.class});
// 設定私有建構函數可訪問!
cons.setAccessible(true);
cons.newInstance(new Object[] {"Alien", 24});
cons = c4.getDeclaredConstructor();
cons.newInstance();
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException |
InvocationTargetException e1) {
e1.printStackTrace();
}
}
```
> ![image](https://hackmd.io/_uploads/BkcqF5PdA.png)
## 類的方法 [Method](https://developer.android.com/reference/java/lang/reflect/Method)
前面我們有介紹到 Class 類中有保存方法資訊(Method information),在這裡我們就可以透過 Class 類來取得方法資訊
> 「**透過 `Method` 物件來表示類的方法**」
### 取得類的「全部」Method
* 從 Class 類中可以取得類中的方法資訊,一般來講我們可以透過以下方法取得(如下表)
| Class 類取得類的方法 | 訪問範圍 |
| ----------------------------- | ---------------------------------------------------------------- |
| `getMethods()` | **限定** 物件 `public` 方法 ==**包含父類方法**== |
| `getDeclaredMethods()` | 物件**全部**方法包括 static and private 方法,==**限定該類**== |
使用範例如下:
1. **目標類**:在這個類中,我們設計 ^1.^ `BaseMethod` 作為父類方法並且其中有 `public`、`protected`、`public` 方法,^2.^ 另外讓 `MyMethod` 繼承 `BaseMethod` 方法,觀察每個反射方法涉及的範圍
```java=
class BaseMethod {
void packageFunc() {
System.out.println("Package function");
}
protected void protectedFunc() {
System.out.println("Protected function");
}
public void publicFunc() {
System.out.println("Public function");
}
}
class MyMethod extends BaseMethod {
private int a = 0;
private static int b = 0;
public void print() {
System.out.println("a is " + a + ", b is " + b);
}
public void setA(int a) {
this.a = a;
}
private static void setB(int b) {
MyMethod.b = b;
}
}
```
2. **反射目標類的方法**:
* 透過 Class 類的 `getMethods()` 方法取得目標類的所有 `public` 方法,其中也包括「父類」的 `public` 方法
```java=
public static void main(String[] args) {
MyMethod my = new MyMethod();
Class<?> clz = my.getClass();
Method[] ms = clz.getMethods();
System.out.println("clz.getMethods(): " + ms.length);
for(Method m : ms) {
System.out.println("Method name: " + m.getName());
}
```
從下圖中,我們也可以觀察到,除了目標類的 `public` 父類的方法的確就只能讀取到 `public` 方法(`publicFunc`)
> ![image](https://hackmd.io/_uploads/rkwfRsvuA.png)
* 透過 Class 類的 `getDeclaredMethods()` 方法取得目標類的「所有方法」,不包括父類方法
```java=
public static void main(String[] args) {
MyMethod my = new MyMethod();
Class<?> clz = my.getClass();
Method[] dms = clz.getDeclaredMethods();
System.out.println("clz.getDeclaredMethods(): " + dms.length);
for(Method m : dms) {
System.out.println("Method name: " + m.getName());
}
}
```
下圖中,我們可以看到它會取得所有的方法(**包括靜態、私有的方法**)
> ![image](https://hackmd.io/_uploads/BkeqynwuR.png)
### 取得類的「指定」Method
* 同樣的,我們也可以透過 Class 類取得指定的方法的 Method 物件,如下表所示
| Class類取得類的方法 | 參數解釋 |
| ------------------------------------------- | ------------------------------------------------------------------------------------- |
| `getMethod(String, Class...<\>)` | 指定方法名稱,Class 為引數的類,==**訪問限定**物件的 public 方法 (包含父類)== |
| `getDeclaredMethod(String, Class...<?>)` | 同上,但方法包括當前類的所有方法(限定當前類,不包括父類) |
範例如下:
1. **目標類**:
```java=
class BaseMethod {
void packageFunc() {
System.out.println("Package function");
}
protected void protectedFunc() {
System.out.println("Protected function");
}
public void publicFunc() {
System.out.println("Public function");
}
}
class MyMethod extends BaseMethod {
private int a = 0;
private static int b = 0;
public void print() {
System.out.println("a is " + a + ", b is " + b);
}
public void setA(int a) {
this.a = a;
}
private static void setB(int b) {
MyMethod.b = b;
}
}
```
2. **反射目標類的方法**:
以下我們透過 `getMethod`、`getDeclaredMethod` 方法指定方法名來取得 Method 物件(說明請看註解)
```java=
public static void main(String[] args) {
MyMethod my = new MyMethod();
Class<?> clz = my.getClass();
try {
// 取得 public 父類方法
Method ms = clz.getMethod("publicFunc");
System.out.println("Parent Method name: " + ms.getName());
// 取得自己的 public 方法
Method ms2 = clz.getMethod("print");
System.out.println("Method name: " + ms2.getName());
// 取得自己的方法
Method dms = clz.getDeclaredMethod("setA", new Class[] {int.class});
System.out.println("Declared Method name: " + dms.getName());
// 取得自己的靜態、私有方法
Method dms2 = clz.getDeclaredMethod("setB", new Class[] {int.class});
System.out.println("Declared static Method name: " + dms2.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
```
> ![image](https://hackmd.io/_uploads/Hy3Y8hPuR.png)
### 透過 Method 呼叫方法
* 當我們取得 Method 物件後,我們就可以透過 Method#`invoke(...)` 來呼叫該方法,如下表所示
| Class類取得類的方法 | 參數解釋 |
| ------------------------------------------- | ------------------------------------------------------------------------------------- |
| `invoke(Object obj, Object... args)` | **第一個 `Object` 是目標物件的實例**,之後的參數則是呼叫該方法時要傳入的參數 |
1. **目標類**:
```java=
class BaseMethod {
void packageFunc() {
System.out.println("Package function");
}
protected void protectedFunc() {
System.out.println("Protected function");
}
public void publicFunc() {
System.out.println("Public function");
}
}
class MyMethod extends BaseMethod {
private int a = 0;
private static int b = 0;
public void print() {
System.out.println("a is " + a + ", b is " + b);
}
public void setA(int a) {
this.a = a;
}
private static void setB(int b) {
MyMethod.b = b;
}
}
```
2. **反射目標類**:
:::warning
* 在使用 `invoke(...)` 調用原來類的方法時,第一個參數需要是目標物件的實例,並且如果方法並非是 `public` 方法,那就需要使用 `setAccessible(true)` 方法,把該方法設定為可訪問
> 並免出現 `IllegalAccessException` 異常
:::
```java=
public static void main(String[] args) {
MyMethod my = new MyMethod();
Class<?> clz = my.getClass();
try {
Method ms = clz.getMethod("publicFunc");
ms.setAccessible(true);
ms.invoke(my);
Method ms2 = clz.getMethod("print");
ms2.setAccessible(true);
ms2.invoke(my);
Method dms = clz.getDeclaredMethod("setA", new Class[] {int.class});
dms.setAccessible(true);
dms.invoke(my, 123);
ms2.invoke(my);
Method dms2 = clz.getDeclaredMethod("setB", new Class[] {int.class});
dms2.setAccessible(true);
dms2.invoke(my, 666);
ms2.invoke(my);
} catch (Exception e) {
e.printStackTrace();
}
}
```
> ![image](https://hackmd.io/_uploads/r1xzhCw_C.png)
## 類的字段 [Field](https://developer.android.com/reference/java/lang/reflect/Field)
前面我們有介紹到 Class 類中會保存字段資訊(Field information),在這裡我們就可以透過 Class 類來取得字段資訊
> 「**透過 `Field` 物件來表示類的字段**」
### 取得全部、指定字段 Field
* 從 Class 類中,我們可以獲得指定類的 ==**字段(位置)**==
| Class 類取得 Field 的方法 | 訪問範圍 |
| ---------------------------- | ---------------------------------------------------------------- |
| `getFields()` | **指定** 物件 `public` 字段(==**包含父類**==) |
| `getField(String)` | 透過字段名稱,取得字段;**訪問指定**物件的 `public` 字段(==**包含父類**==) |
| `getDeclaredFields()` | 物件 **全部字段**;包括 `static`、`private` 字段(==**限定該類**==) |
| `getDeclaredField(String)` | 透過字段名稱,取得字段;全部字段包括 `static`、`private` 變數(==**限定該類**==) |
1. **目標類**:
```java=
class MyMyField {
public int aa = 0;
}
class MyField extends MyMyField {
public int a = 10;
private int b = 20;
private static int c = 30;
int getB() {
return b;
}
}
```
2. **反射目標類**:
```java=
public static void main(String[] args) {
MyField m = new MyField();
Class<?> clz = m.getClass();
// 取得全部 public 字段(包括父類)
Field[] fs = clz.getFields();
System.out.println("clz.getFields: " + fs.length);
for(Field f : fs) {
System.out.println("Field Name: " + f.getName());
}
// 取得全部 public 字段(只限定自身類)
Field[] dfs = clz.getDeclaredFields();
System.out.println("clz.getDeclaredFields: " + dfs.length);
for(Field f : dfs) {
System.out.println("Field Name: " + f.getName());
}
}
```
> ![](https://i.imgur.com/teI6tNB.png)
### 「取得」字段的實例
* 在取得 Field 字段(物件)後,就可以透過 Field#`get(Object)` 方法就可以取得該字段的實例(也就是 取得==真正變數==)
| Field 的方法 | 解釋 |
| ---------------- | ---------------------------------- |
| `get(Object)` | 透過物件,取得變數,**++必須強轉型++** |
| `getInt(Object`) | 同上但不必強轉型,自動轉為 int |
| `getXXX(Object)` | XXX 為基礎型態 |
1. **目標類**:
```java=
class MyField {
public int a = 10;
private int b = 20;
private static int c = 30;
int getB() {
return b;
}
int getC() {
return c;
}
}
```
2. **反射目標類**:(請看註解說明)
```java=
public static void main(String[] args) {
MyField m = new MyField();
Class<?> clz = m.getClass();
try {
// 訪問物件 public 變數
Field fa = MyField.class.getDeclaredField("a");
System.out.println("Instances, access Field a: " + fa.get(m));
// 訪問物件 private 變數要多設置可訪問 setAccessible(true)
Field fb = clz.getDeclaredField("b");
fb.setAccessible(true);
System.out.println("Instances, access Field a: " + fb.getInt(m));
// 也可以訪問 static 變數
Field fc = clz.getDeclaredField("c");
fc.setAccessible(true);
System.out.println("Instances, access Field a: " + fc.getInt(m));
} catch (Exception e) {
e.printStackTrace();
}
}
```
:::success
* **當未實例化時,透過 Class 類也可以訪問物件的變數,但是只能訪問靜態變數 (static params)**,也就是 Object 包括 class
```java=
public static void main(String[] args) {
MyField m = new MyField();
Class<?> clz = m.getClass();
try {
Field fc = MyField.class.getDeclaredField("c");
fc.setAccessible(true);
System.out.println("No Instances, get static Field c: "
+ fc.getInt(MyField.class));
} catch (Exception e) {
e.printStackTrace();
System.out.println("Cannot access non instances object");
}
}
```
:::
> ![](https://i.imgur.com/tlQ0mmm.png)
### 「設定」字段的實例
* 在取得 Field 字段(物件)後,就可以透過 Field#`set(Object, ...)` 方法就可以設定該字段的實例(也就是 設定==變數進實例==)
| Field 的方法 | 解釋 |
| --------------------- | ------------------ |
| `set(Object, value)` | 透過物件,設定變數 |
| `setInt(Object, value)` | 同上 |
| `setXXX(Object, value)` | XXX 為基礎型態 |
1. **目標類**:
```java=
class MyField {
public int a = 10;
private int b = 20;
private static int c = 30;
int getB() {
return b;
}
int getC() {
return c;
}
}
```
2. **反射目標類**:(請看註解說明)
```java=
public static void main(String[] args) {
MyField m = new MyField();
Class<?> clz = m.getClass();
try {
// 兩個參數一個物件、一個是要設定的值
Field fa = MyField.class.getDeclaredField("a");
System.out.println("Instances, access Field a: " + fa.get(m));
fa.set(m, 111);
System.out.println("after change a: " + m.a);
// 當要設定 private 參數時要先設定可訪問,setAccessible(true)
Field fb = clz.getDeclaredField("b");
fb.setAccessible(true);
System.out.println("Instances, access Field a: " + fb.getInt(m));
fb.setInt(m, 222);
System.out.println("after change b: " + m.getB());
// 同樣的,可以設定 static 變數
Field fc = clz.getDeclaredField("c");
fc.setAccessible(true);
System.out.println("Instances, access Field a: " + fc.getInt(m));
fc.setInt(m, 333);
System.out.println("after change c: " + m.getC());
} catch (Exception e) {
e.printStackTrace();
}
}
```
:::success
* static 變數其實也不用透過實例化才能設置參數,可直接透過 Class 類設定
```java=
public static void main(String[] args) {
MyField m = new MyField();
Class<?> clz = m.getClass();
try {
Field fcc = MyField.class.getDeclaredField("c");
fcc.setAccessible(true);
System.out.println("No Instances, get static Field c: "
+ fcc.getInt(MyField.class));
fcc.setInt(MyField.class, 33333);
System.out.println("after change c: " + fcc.getInt(MyField.class));
} catch (Exception e) {
e.printStackTrace();
System.out.println("Cannot access non instances object");
}
}
```
:::
> ![](https://i.imgur.com/EcTevGz.png)
### 反射創建數組
* 透過 `java.lang.reflect.Array` 包,可以使用 Java 實現的反射創建數組功能,範例如下:反射創建 String 數組空間
```java=
import java.lang.reflect.Array;
public class ArrayUsage {
public static void main(String[] args) {
String[] myStr = (String[]) Array.newInstance(String.class, 10);
for (String s : myStr) {
System.out.println("String: " + s);
}
}
}
```
從下圖中,我們可以看到 Java 的確會創建數組空間,**但是不會設定每個元素的內容**
> ![image](https://hackmd.io/_uploads/S1pz-gOu0.png)
## 反射註解 Annotation
反射註解是許多開源框架中會使用到的技巧之一,下面表格為常用於反射判斷註解的 Java Reflect API
| 註解的反射 API 名 | 解釋 |
| ----- | ------ |
| `isAnnotation()` | 判斷類是否有註解 |
| `isAnnotationPresent(Class<? extends Annotation>)` | 判斷 **類是否==應用了某個註解==**|
| `getAnnotation(Class<A>)` | 返回 **註解物件** |
| `getAnnotations()` | 由於一個類、參數上可以有多個註解,所以可以取得多個 Annotation(也就是返回 `Annotation[]`) |
:::success
如果要使用註解反射技巧,那註解就要保留到運行期間!(`@Retention` 註解需設定為 `RUNTIME`)
:::
:::info
如果不清楚「Java 註解」的話,可以點擊這篇連結去了解 [**深入探討 Android、Java 註解應用:分析註解與 Enum 的差異 | Android APT**](https://devtechascendancy.com/android-annotations-enums-guide/)
:::
### 反射 Class 類的註解:證明 Runtime 期間
* 反射 `Class` 類的註解,在這裡我們要證明「所有的註解要反射,都需要保留到 Runtime 期間才能反射」,範例如下
* **定義 Annotation 類**:這邊我們定義兩個特性的註解,一個保留到 `RUNTIME`,一個保留到 `CLASS`
> 使用反射必須使用元註解(使用 `@Retention` 註解)
```java=
// 保存到 RUNTIME
@Retention(RetentionPolicy.RUNTIME)
@interface ReAnTest_1 {
int age() default 18;
String name() default "Alien";
}
// 保存到 CLASS
@Retention(RetentionPolicy.CLASS)
@interface ReAnTest_2 {
int age() default 18;
String name() default "Alien";
}
```
* **反射 Annotation 類**:
```java=
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ReAnTest_1(age = 20, name = "Pan")
@ReAnTest_2(age = 21, name = "Pana")
public class reflectionAnnotation {
public static void main(String[] args) {
Class<?> clz = reflectionAnnotation.class;
if(clz.isAnnotation()) {
System.out.println("This class isAnnotation");
}
if(clz.isAnnotationPresent(ReAnTest_2.class)) {
System.out.println("This class Annotation by ReAnTest_2");
} else {
System.out.println("This class Annotation \"not\" ReAnTest_2");
}
if(clz.isAnnotationPresent(ReAnTest_1.class)) {
System.out.println("This class Annotation by ReAnTest_1");
// getAnnotation(Class) 可以 **動態取得該類的註解物件**,並取得其值
ReAnTest_1 r = clz.getAnnotation(ReAnTest_1.class);
System.out.println("Age: " + r.age());
System.out.println("Name: " + r.name());
} else {
System.out.println("This class Annotation \"not\" ReAnTest_1");
}
}
}
```
從下圖中我們可以看到,同樣都被註解,但是 **只有保存到 `RUNTIME`(ReAnTest_1 註解)的註解才能被反射偵測到,而 `CLASS`(`ReAnTest_2` 註解)則會被消除**
> ![](https://i.imgur.com/g1DLUi6.png)
### 反射方法上的註解
* 如果要提取方法上的註解,首先就需要先透過 Class 類提取 Method 物件,再透過 Method 物件取得方法上的註解,範例如下:
* **定義 Annotation 類**:
```java=
@Retention(RetentionPolicy.RUNTIME)
@interface ReAnTest_1 {
int age() default 18;
String name() default "Alien";
}
```
* **將 `ReAnTest_1` 註解用類中的方法上**:
```java=
class MyAnnotationClass {
private int age = 10;
private String name = "kyle";
@ReAnTest_1(age = 20, name = "Pan")
void MyFunction() {
System.out.println("age : " + age);
System.out.println("name : " + name);
}
}
```
* **使用反射取得方法上註解的內容**:透過 Class 類取得 Method,並且透過 `getAnnotation(...)` 方法取得指定註解以及資訊
```java=
public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException, NoSuchFieldException {
MyAnnotationClass m = new MyAnnotationClass();
Class<?> clz = m.getClass();
Method method = clz.getDeclaredMethod("MyFunction");
if(method.isAnnotationPresent(ReAnTest_1.class)) {
Field AGE = clz.getDeclaredField("age");
Field NAME = clz.getDeclaredField("name");
AGE.setAccessible(true);
NAME.setAccessible(true);
int age = AGE.getInt(m);
String name = (String) NAME.get(m);
System.out.println("Original age: " + age + ", name: " + name);
ReAnTest_1 r = method.getAnnotation(ReAnTest_1.class);
AGE.setInt(m, r.age());
NAME.set(m, r.name());
System.out.println("Change it by Annotation");
method.invoke(m);
} else {
method.invoke(m);
}
}
```
> ![](https://i.imgur.com/Xo3ovRD.png)
### 反射 Field 註解
* 同樣的,註解也可以使用類的字段上
如果要提取字段上的註解,首先就需要先透過 Class 類提取 Field 物件,再透過 Field 物件取得字段上的註解,範例如下:
> 以下用 Android 來測試,使用反射來做 `findViewById()` 的行為,並做出設定文字
* **定義 Annotation 類**:
```java=
// 註解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface MyAnnotation {
@IdRes int id() default -1;
}
```
* **使用 `@MyAnnotation` 註解在類的字段上**
```java=
public class MainActivity extends AppCompatActivity {
@MyAnnotation(id = R.id.sample_text)
TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyClass.Inject_by_reflection(this);
tv.setText("Yeah 123");
}
}
```
* **使用反射取得字段上註解的內容**:取得內容後(這個內容就是 Layout ID),就可以使用它來取得對應的 View,並且設定其設定到註解的參數上!
```java=
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Field;
public class MyClass {
public static void Inject_by_reflection(Activity activity) {
Class<? extends Activity> clz = activity.getClass();
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
// Retention Runtime !!
MyAnnotation anno = field.getAnnotation(MyAnnotation.class);
if(anno == null) {
return;
}
int id = anno.id();
if (id != -1) {
View view = activity.findViewById(id);
try {
field.set(activity, view); // (物件,設定已更改的內容)
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
```
## 反射泛型 Generic
**當對於一個泛型類進行反射時,需要透過 Type 體系**,該體系由 5 個界面組成(`Type` 為基礎界面),如下表所示
| 對於泛型,反射提供的界面 | 功能 | Function |
| -------- | -------- | -------- |
| Type | 泛型名稱 | getTypeName |
| TypeVariable | **泛型細節**,泛型名稱、全類名,可搜尋到++上限++的類 | getName \ getGenericDeclaration \ getBounds |
| ParameterizedType | **泛型類**,返回具體的類型類型,可取得原數據中泛型簽名類型 (泛型真實類型) | getActualTypeArguments \ getRawType \ getOwnerType |
| GenericArrayType | 參數類型為 **泛型數組** 時(List[]\Map[]) | getGenericComponentType |
| WildcardType | **通配符**,獲取通配符泛型++上下限++ | getUpperBounds \ getLowerBounds |
:::info
* 這些繼承 `Type` 界面的子界面 **分別實現了++不同泛型對應的參數++**
> ![](https://i.imgur.com/nkyjNH8.png)
:::
### getGenericType 取得泛型資訊:TypeVariable 捕捉編階
* 透過 Field#`getTypeName`、`getGenericType()` 方法可以取得保留在 Class 類中字段的「泛型資訊」
1. Type#`getTypeName()`、TypeVariable#`getName()` 方法取得的是一個泛型符號,而不是真正的類
2. 透過 TypeVariable#`getBounds()` 方法可以取得泛型的界線數組,之所以是數組是因為泛型可以有多個限制
:::info
這個範例中,我們來抓取泛型 [**限定類型 Qulified Type**](https://devtechascendancy.com/java-generics-complete-guide/#%E9%99%90%E5%AE%9A%E9%A1%9E%E5%9E%8B_Qulified_Type) 的邊界,該範例規範上界至少要實作 `Cloneable` 界面
:::
* 如果有明確的上界則返回上界的類型
* 沒有明確的上界則是返回 Object 類型
```java=
class MyBookMark implements Cloneable{}
// 邊界為 Cloneable
public class TestType<K extends Cloneable, V> {
K key;
V value;
public static void main(String[] args) throws NoSuchFieldException, SecurityException {
TestType<MyBookMark, Integer> book = new TestType<>();
Field fk = book.getClass().getDeclaredField("key"); // K
Field fv = book.getClass().getDeclaredField("value"); // V
TypeVariable<?> keyType = (TypeVariable<?>) fk.getGenericType();
TypeVariable<?> valueType = (TypeVariable<?>) fv.getGenericType();
// Type 界面的 getTypeName 方法
System.out.println("Type's getTypeName: " + keyType.getTypeName());
System.out.println("Type's getTypeName: " + valueType.getTypeName() + "\n");
// TypeVariable 界面的 getName 方法
System.out.println("TypeVariable's getName: " + keyType.getName());
System.out.println("TypeVariable's getName: " + valueType.getName() + "\n");
// TypeVariable 界面的 getGenericDeclaration 方法
System.out.println("TypeVariable's getGenericDeclaration: " + keyType.getGenericDeclaration());
System.out.println("TypeVariable's getGenericDeclaration: " + valueType.getGenericDeclaration() + "\n");
// TypeVariable 界面的 getBounds 方法,返回數組
for(Type t : keyType.getBounds()) { //"2. "
System.out.println("TypeVariable's getBounds: " + t.toString());
}
for(Type t : valueType.getBounds()) {
System.out.println("TypeVariable's getBounds: " + t.toString());
}
}
}
```
從下圖來看,我們也確實可以看到透過反射界面 `TypeVariable` 提供的方法,確實可以捕捉到泛型的邊界
> ![](https://i.imgur.com/cWvEhLK.png)
### ParameterizedType 具體類型
* 反射透過 `ParameterizedType` 界面提供的功能,我們可以捕捉擁有泛型參數的具體類型(透過 `getActualTypeArguments()` 方法)
:::info
它與 `TypeVariable` 不同,它只需要透過指定泛型類型,就可以取得該泛型類型的確切泛型資訊
:::
範例如下
```java=
public class TestParamType {
Map<String, Integer> map;
public static void main(String[] args) throws NoSuchFieldException, SecurityException {
Field f = TestParamType.class.getDeclaredField("map");
ParameterizedType pType = (ParameterizedType) f.getGenericType();
System.out.println("ParameterizedType: " + pType);
System.out.println("getRawType: " + pType.getRawType()); // 返回代表的 class
System.out.println("getOwnerType: " + pType.getOwnerType());
for(Type t : pType.getActualTypeArguments()) { // 獲得具體類型
System.out.println("getActualTypeArguments: " + t);
}
}
}
```
> ![](https://i.imgur.com/3sNSi3G.png)
### GenericArrayType 泛型數組
* 對於 `List` 相關類型的泛型,可以使用反射的 `GenericArrayType` 界面捕捉泛型資訊
範例如下:
```java=
import java.lang.reflect.*;
import java.util.List;
public class ArrayType {
List<String>[] lists;
public static void main(String[] args) throws NoSuchFieldException, SecurityException {
Field ff = ArrayType.class.getDeclaredField("lists");
GenericArrayType genericType = (GenericArrayType) ff.getGenericType();
System.out.println("getGenericComponentType: " + genericType.getGenericComponentType());
}
}
```
> ![](https://i.imgur.com/aTsNbuB.png)
### WildcardType 通配符
:::info
如果對泛型的「[**通配符**](https://devtechascendancy.com/java-generics-complete-guide/#%E9%80%9A%E9%85%8D%E7%AC%A6%E5%8A%9F%E8%83%BD%EF%BC%9A%E6%B3%9B%E5%9E%8B%E9%A1%9E%E4%B9%8B%E9%96%93%E7%9A%84%E9%97%9C%E4%BF%82)」不了解,可以先點擊連結
:::
* 要獲取通配符的上下限,需要先透過 `ParameterizedType` 取得泛型類的具體類型,透過再它取得通配符的資訊(使用 `getActualTypeArguments()` 方法)
```java=
import java.lang.reflect.*;
import java.util.List;
public class WildType {
List<? extends Number> a; // 上界
List<? super String> b; // 下界
public static void main(String[] args) throws NoSuchFieldException, SecurityException {
Field fa = WildType.class.getDeclaredField("a");
Field fb = WildType.class.getDeclaredField("b");
// 1. 先獲取泛型實體
ParameterizedType pa = (ParameterizedType) fa.getGenericType();
ParameterizedType pb = (ParameterizedType) fb.getGenericType();
System.out.println("pa: " + pa.getTypeName() + ", getRawType" + pa.getRawType());
System.out.println("pb: " + pb.getTypeName() + ", getRawType" + pb.getRawType());
// 2. 從泛型中拿到通配符
WildcardType wTypeA = (WildcardType) pa.getActualTypeArguments()[0]; // 可能有多個上下限
WildcardType wTypeB = (WildcardType) pb.getActualTypeArguments()[0];
System.out.println(wTypeA.getUpperBounds()[0]);// 可能有多個上下限
System.out.println(wTypeB.getLowerBounds()[0]);// 可能有多個上下限
}
}
```
> ![](https://i.imgur.com/bXMqgx7.png)
## Appendix & FAQ
:::info
:::
###### tags: `Java 基礎進階` `反射`