如有引用參考請詳註出處,感謝
反射是運行 Running Time
的時候才會創建、尋找類,或是方法、建構子、屬性
反射是 Java 被視為動態語言的關鍵,在運行期借助 Reflection API 進行反射
在程序中一般的類是在編譯期間就確定 (泛型例外),而反射基置創建的物件是運行時才確定
如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 DevTech Ascendancy Hub
本篇文章對應的是 深入探索 Java 反射:理解並使用 Class 類 | Constructor、Method、Field、Annotation 和反射泛型
JVM 加載 .class
檔案之後會在 JVM 中建立一個 Class 類
,而 Class 類是指在「運行期間」的「物件」,這個物件內部會有類所有的描述(包括 Source Code、Annotation、Header、Static Field、Field … 等等資訊)
為什麼要保留這些資訊?所有的語言都有嗎?
不,並非所有的語言都會保留這些訊息(像 C 語言就沒有),像是更針對執行速度、在意應用大小的語言就不會保留這些資訊
這些訊息我們會稱之為 MetaData,而針對 MetaData 進行操作的行為就稱之為「
Meta Programming
」
而保留這些訊息的程式語言(像是我們說的 Java)就可以更具有拓展性、自由度
認識 .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
)
這裡先說明如何透過程式取得 Class 類,之後章節再說明取得 Class 類之後可以做些什麼事… 我們可以透過以下三種方式來取得 Class 類
,範例如下:
透過指定「類名」直接取得指定的 Class 類
透過 Class#forName
方法,並輸入完整的類路徑來取得 Class 類
透過實例物件(instance
)的 getClass()
方法來取得 Class 類
Java 有提供一個標準包用來分析 MetaData 並使用,該包在 java.lang.reflect
中,該包提供了一組用於反射(Reflection
)操作的類和界面
簡單來說反射就是:是一種允許程序在運行時檢查和修改其自身結構的功能
這些類和界面使得程序可以動態地獲取類的結構信息(如類名、方法、字段、構造函數等),並且可以在運行時調用方法、訪問字段和創建實例
反射的主要類
主要類 | 概述 |
---|---|
Constructor |
代表類的建構函數,提供方法來創建新實例,包括傳遞參數 |
Method |
提供方法來調用方法,包括傳遞參數、獲取返回值 |
Field |
提供方法來讀取和設置字段的值,無論字段是私有、保護還是公共的 |
Array |
提供靜態方法來動態創建和操作數組 |
反射的主要界面
主要類 | 概述 |
---|---|
Type |
所有類型的公共界面 |
InvocationHandler |
用於處理代理實例上的方法調用 |
GenericArrayType (用來處理泛型) |
代表泛型數組類型 |
ParameterizedType (用來處理泛型) |
代表參數化類型 |
TypeVariable (用來處理泛型) |
代表類型變量,是泛型中的一部分 |
WildcardType (用來處理泛型) |
代表通配符類型,是泛型中的一部分 |
反射會消耗一定的系統資源,多少會影響應用效能
反射調用可 忽略權限檢查,可能會破壞封裝導置安全問題
另外,對於可設定混淆的應用(像是 Android App 應用),就要特別小心!有使用反射技巧的程式,要記得設定跳過混淆,否則會造成框架無法正常運行!!
因為很多框架會依賴反射機制,而混淆會導致 .class
類中保存的訊息與我們認知的訊息不同
前面我們有介紹到 Class 類中有保存建構器(也就是構造函數的資訊),在這裡我們就可以透過建構器來實例化類別,而不用透過 new
關鍵字來獲得實例
透過 Class#newInstance()
方法可以直接創建一個 無參的建構函數的實例
範例如下:
目標類:目標類為一個無參數的 public
建構函數
反射目標類:透過 Class#newInstance
方法直接處實例化物件,實例化後就可以以一般操作物件的方式使用
使用 newInstance
這種方式實例化物件時需要注意以下事項
只能實例化無參建構函數,如果建構函數有參數,則會實例化失敗
無法實例化使用 private
關鍵字描述的建構函數,否則會產生 IllegalAccessException
錯誤
另外我們可以透過 Class 取得 Constructor
類,透過這個方法我們就可以取得 Class 類中對於建構函數所有的描述,並且可以透過它來實例化類別
「透過
Constructor
物件來表示類的建構函數」
Class 取得 Constructor 的方式 |
概述說明 |
---|---|
getConstructors() |
取得所有公開的建構函數(不包括父類) |
getDeclaredConstructors() |
取得所有建構函數,限定於該類(不包括父類) |
以下範例為使用 getDeclaredConstructors()
方法取得 Constructor 物件並使用
範例如下:
目標類:該目標類中有兩個建構函數,一個是 public
的建構函數,以及另一個 private
描述的有參構造函數
反射目標類:
Constructor 實例化類別比較特殊,它可以訪問私有建構函數,不過必須透過 setAccessible() 設定為可放問(非私有的可以不用設定)
Constructor
可實例化有參、私有建構函數,可透過 new Object[] {}
物件傳入參數
public T newInstance(Object... initargs)
以下範例為使用 getConstructors()
方法取得 Constructor 物件並使用,它的特點在於 1. 只取得 public
建構函數、2. 也同時可以取得所有父類 public
建構函數
範例如下:
目標類:在這裡我們設計 Constructor_3
類,並且開類擁有三種不同訪問權的建構函數(分別是 package
、public
、private
三種訪問權),用來觀察 getConstructors()
函數可取得的建構函數
反射目標類:
我們觀察是否都是取得 public
訪問權的建構函數(這裡我們觀察數量)
如下圖中我們可以看到取得的建構類(Constructor
)確實只有一個
在上面小節的案例中,我們都是一次性獲取所有的建構函數(getConstructors()
、getDeclaredConstructors()
函數),但其實我們透過 Class 陣列
來指定參數類型,並獲取指定的建構函數
範例如下:
目標類:在這裡我們設計 Constructor_4
類,並設計不同的建構函數,並且每個建構函數有不同的參數(入參)
反射目標類:
指定建構函數時(使用 getDeclaredConstructor(...)
方法)可以透過 Class<?> 陣列
來指定類的接收參數的類型
如果要訪問非 public
的建構函數時,需要透過 setAccessible(true)
讓該建構函數可訪問,否則會拋出 IllegalAccessException
異常
如果是基礎類就傳基礎類,而不是基礎類的包裝類
也就是假設參數類型為 int
,那就傳入 int.class
而不是 Integer.class
(因為它們是不同的類)
透過 Constructor
建構物件時,也有要傳入對應的類型、順序的參數(經由 Object array
呼叫指定建構函數)
前面我們有介紹到 Class 類中有保存方法資訊(Method information),在這裡我們就可以透過 Class 類來取得方法資訊
「透過
Method
物件來表示類的方法」
從 Class 類中可以取得類中的方法資訊,一般來講我們可以透過以下方法取得(如下表)
Class 類取得類的方法 | 訪問範圍 |
---|---|
getMethods() |
限定 物件 public 方法 包含父類方法 |
getDeclaredMethods() |
物件全部方法包括 static and private 方法,限定該類 |
使用範例如下:
目標類:在這個類中,我們設計 1. BaseMethod
作為父類方法並且其中有 public
、protected
、public
方法,2. 另外讓 MyMethod
繼承 BaseMethod
方法,觀察每個反射方法涉及的範圍
反射目標類的方法:
透過 Class 類的 getMethods()
方法取得目標類的所有 public
方法,其中也包括「父類」的 public
方法
從下圖中,我們也可以觀察到,除了目標類的 public
父類的方法的確就只能讀取到 public
方法(publicFunc
)
透過 Class 類的 getDeclaredMethods()
方法取得目標類的「所有方法」,不包括父類方法
下圖中,我們可以看到它會取得所有的方法(包括靜態、私有的方法)
同樣的,我們也可以透過 Class 類取得指定的方法的 Method 物件,如下表所示
Class類取得類的方法 | 參數解釋 |
---|---|
getMethod(String, Class...<\>) |
指定方法名稱,Class 為引數的類,訪問限定物件的 public 方法 (包含父類) |
getDeclaredMethod(String, Class...<?>) |
同上,但方法包括當前類的所有方法(限定當前類,不包括父類) |
範例如下:
目標類:
反射目標類的方法:
以下我們透過 getMethod
、getDeclaredMethod
方法指定方法名來取得 Method 物件(說明請看註解)
當我們取得 Method 物件後,我們就可以透過 Method#invoke(...)
來呼叫該方法,如下表所示
Class類取得類的方法 | 參數解釋 |
---|---|
invoke(Object obj, Object... args) |
第一個 Object 是目標物件的實例,之後的參數則是呼叫該方法時要傳入的參數 |
目標類:
反射目標類:
在使用 invoke(...)
調用原來類的方法時,第一個參數需要是目標物件的實例,並且如果方法並非是 public
方法,那就需要使用 setAccessible(true)
方法,把該方法設定為可訪問
並免出現
IllegalAccessException
異常
前面我們有介紹到 Class 類中會保存字段資訊(Field information),在這裡我們就可以透過 Class 類來取得字段資訊
「透過
Field
物件來表示類的字段」
從 Class 類中,我們可以獲得指定類的 字段(位置)
Class 類取得 Field 的方法 | 訪問範圍 |
---|---|
getFields() |
指定 物件 public 字段(包含父類) |
getField(String) |
透過字段名稱,取得字段;訪問指定物件的 public 字段(包含父類) |
getDeclaredFields() |
物件 全部字段;包括 static 、private 字段(限定該類) |
getDeclaredField(String) |
透過字段名稱,取得字段;全部字段包括 static 、private 變數(限定該類) |
目標類:
反射目標類:
在取得 Field 字段(物件)後,就可以透過 Field#get(Object)
方法就可以取得該字段的實例(也就是 取得真正變數)
Field 的方法 | 解釋 |
---|---|
get(Object) |
透過物件,取得變數,必須強轉型 |
getInt(Object ) |
同上但不必強轉型,自動轉為 int |
getXXX(Object) |
XXX 為基礎型態 |
目標類:
反射目標類:(請看註解說明)
當未實例化時,透過 Class 類也可以訪問物件的變數,但是只能訪問靜態變數 (static params),也就是 Object 包括 class
在取得 Field 字段(物件)後,就可以透過 Field#set(Object, ...)
方法就可以設定該字段的實例(也就是 設定變數進實例)
Field 的方法 | 解釋 |
---|---|
set(Object, value) |
透過物件,設定變數 |
setInt(Object, value) |
同上 |
setXXX(Object, value) |
XXX 為基礎型態 |
目標類:
反射目標類:(請看註解說明)
static 變數其實也不用透過實例化才能設置參數,可直接透過 Class 類設定
透過 java.lang.reflect.Array
包,可以使用 Java 實現的反射創建數組功能,範例如下:反射創建 String 數組空間
從下圖中,我們可以看到 Java 的確會創建數組空間,但是不會設定每個元素的內容
反射註解是許多開源框架中會使用到的技巧之一,下面表格為常用於反射判斷註解的 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 期間才能反射」,範例如下
定義 Annotation 類:這邊我們定義兩個特性的註解,一個保留到 RUNTIME
,一個保留到 CLASS
使用反射必須使用元註解(使用
@Retention
註解)
反射 Annotation 類:
從下圖中我們可以看到,同樣都被註解,但是 只有保存到 RUNTIME
(ReAnTest_1 註解)的註解才能被反射偵測到,而 CLASS
(ReAnTest_2
註解)則會被消除
如果要提取方法上的註解,首先就需要先透過 Class 類提取 Method 物件,再透過 Method 物件取得方法上的註解,範例如下:
定義 Annotation 類:
將 ReAnTest_1
註解用類中的方法上:
使用反射取得方法上註解的內容:透過 Class 類取得 Method,並且透過 getAnnotation(...)
方法取得指定註解以及資訊
同樣的,註解也可以使用類的字段上
如果要提取字段上的註解,首先就需要先透過 Class 類提取 Field 物件,再透過 Field 物件取得字段上的註解,範例如下:
以下用 Android 來測試,使用反射來做
findViewById()
的行為,並做出設定文字
定義 Annotation 類:
使用 @MyAnnotation
註解在類的字段上
使用反射取得字段上註解的內容:取得內容後(這個內容就是 Layout ID),就可以使用它來取得對應的 View,並且設定其設定到註解的參數上!
當對於一個泛型類進行反射時,需要透過 Type 體系,該體系由 5 個界面組成(Type
為基礎界面),如下表所示
對於泛型,反射提供的界面 | 功能 | Function |
---|---|---|
Type | 泛型名稱 | getTypeName |
TypeVariable | 泛型細節,泛型名稱、全類名,可搜尋到上限的類 | getName \ getGenericDeclaration \ getBounds |
ParameterizedType | 泛型類,返回具體的類型類型,可取得原數據中泛型簽名類型 (泛型真實類型) | getActualTypeArguments \ getRawType \ getOwnerType |
GenericArrayType | 參數類型為 泛型數組 時(List[]\Map[]) | getGenericComponentType |
WildcardType | 通配符,獲取通配符泛型上下限 | getUpperBounds \ getLowerBounds |
這些繼承 Type
界面的子界面 分別實現了不同泛型對應的參數
透過 Field#getTypeName
、getGenericType()
方法可以取得保留在 Class 類中字段的「泛型資訊」
Type#getTypeName()
、TypeVariable#getName()
方法取得的是一個泛型符號,而不是真正的類
透過 TypeVariable#getBounds()
方法可以取得泛型的界線數組,之所以是數組是因為泛型可以有多個限制
這個範例中,我們來抓取泛型 限定類型 Qulified Type 的邊界,該範例規範上界至少要實作 Cloneable
界面
如果有明確的上界則返回上界的類型
沒有明確的上界則是返回 Object 類型
從下圖來看,我們也確實可以看到透過反射界面 TypeVariable
提供的方法,確實可以捕捉到泛型的邊界
反射透過 ParameterizedType
界面提供的功能,我們可以捕捉擁有泛型參數的具體類型(透過 getActualTypeArguments()
方法)
它與 TypeVariable
不同,它只需要透過指定泛型類型,就可以取得該泛型類型的確切泛型資訊
範例如下
對於 List
相關類型的泛型,可以使用反射的 GenericArrayType
界面捕捉泛型資訊
範例如下:
如果對泛型的「通配符」不了解,可以先點擊連結
要獲取通配符的上下限,需要先透過 ParameterizedType
取得泛型類的具體類型,透過再它取得通配符的資訊(使用 getActualTypeArguments()
方法)
Java 基礎進階
反射