Java 反射詳解

OreChou發表於2018-08-10

Java 反射是一個比較重要的知識點,你會在很多地方見到反射。它提供了 Java 語言在執行期間載入、探知和使用編譯期間完全未知的類的能力。這種能力在框架的編寫中非常常見,例如動態代理中、類掃描解析中。

反射的定義與作用

反射機制:即 Java 語言在執行時有一種自觀的能力,能夠了解自身的情況併為下一步的動作做準備。反應出來就是,在執行時,對於一個類,我們能夠知道該類有哪些方法和屬性。對於一個物件,我們能夠呼叫其任意的一個方法。這是一種動態獲取類的資訊以及動態呼叫物件方法的能力。

實現反射的基礎

Java 提供反射機制,依賴於 Class 類和 java.lang.reflect 類庫。其主要的類如下:

  1. Class:表示類或者介面
  2. Field:表示類中的成員變數
  3. Method:表示類中的方法
  4. Constructor:表示類的構造方法
  5. Array:該類提供了動態建立陣列和訪問陣列元素的靜態方法

Class

Class 類是 Java 中用來表示執行時型別資訊的對應類。實際上在 Java 中每個類都有一個 Class 物件,每當我們編寫並且編譯一個新建立的類就會將相關資訊寫到 .class 檔案裡。當我們 new 一個新物件或者引用靜態成員變數時,JVM 中的類載入器子系統會將對應 Class 物件載入到 JVM 中,然後 JVM 再根據這個型別資訊相關的 Class 物件建立我們需要例項物件或者提供靜態變數的引用值。我們可以將 Class 類,稱為類型別,一個 Class 物件,稱為類型別物件(參考 《Thinking in Java》)。

Class 類有以下的特點:

  1. Class 類也是類的一種,class 則是關鍵字。
  2. Class 類只有一個私有的建構函式,只有 JVM 能夠建立 Class 類的例項。
  3. 對於同一類的物件,在 JVM 中只有唯一一個對應的 Class 類例項來描述其型別資訊。(同一個類:即包名 + 類名相同,且由同一個類載入器載入)

.class 檔案儲存了一個 Class 的所有資訊,比如所有的方法,所有的建構函式,所有的欄位(成員屬性)等等。JVM 啟動的時候通過 .class 檔案會將相關的類載入到記憶體中,過程如下:

Java 反射詳解
)

獲取 Class 例項的方法

上面提到 Class 類只有一個私有的建構函式。所以無法通過 new 的方法獲取 Class 例項。

/*
 * Constructor. Only the Java Virtual Machine creates Class
 * objects.
 */
private Class() {}
複製程式碼
Class.forName() 方法

可以通過 Class 的 forName 方法獲取 Class 例項,其中類的名稱要寫類的完整路徑。該方法只能用於獲取引用型別的類型別物件。

// 這種方式會使用當前的類的載入器載入,並且會將 Class 類例項初始化
Class<?> clazz = Class.forName("java.lang.String");
// 上面的呼叫方式等價於
Class<?> clazz = Class.forName("java.lang.String", true, currentLoader);
複製程式碼

使用該方法可能會丟擲 ClassNotFoundException 異常,這個異常發生在類的載入階段,原因如下:

  1. 類載入器在類路徑中沒有找到該類(檢查:檢視所在載入的類以及其所依賴的包是否在類路徑下)
  2. 該類已經被某個類載入器載入到 JVM 記憶體中,另外一個類載入器又嘗試從同一個包中載入
Object.getClass() 方法

如果我們有一個類的物件,那麼我們可以通過 Object.getClass 方法獲得該類的 Class 物件。

// String 物件的 getClass 方法
Class clazz1 = "hello".getClass();
// 陣列物件的 getClass 方法
Class clazz2 = (new byte[1024]).getClass();
System.out.println(class2) // 會輸出 [B, [ 代表是陣列, B 代表是 byte。即 byte 陣列的類型別
複製程式碼
class 語法

若我們知道要獲取的類型別的名稱時,我們可以使用 class 語法獲取該類型別的物件。

// 類
Class clazz = Integer.class;
// 陣列
Class clazz2 = int [][].class;
複製程式碼
包裝類的 TYPE 靜態屬性

對於基本型別和 void 都有對應的包裝類。在包裝類中有一個靜態屬性 TYPE,儲存了該來的類型別。以 Integer 類為例,其原始碼中定義瞭如下的靜態屬性:

/**
 * The {@code Class} instance representing the primitive type
 * {@code int}.
 *
 * @since   JDK1.1
 */
@SuppressWarnings("unchecked")
public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");
複製程式碼

生成 Class 類例項的方法:

Class clazz1 = Integer.TYPE;
Class clazz2 = Void.TYPE;
複製程式碼
Class 類的方法

Class 中有獲取其他 Class 的方法,列舉如下:

  1. Class.getSuperclass():獲取該類的父類
  2. Class.getClasses() :獲取該類所有公共類、介面、列舉組成的Class 陣列,包括繼承的
  3. Class.getDeclaredClasses():獲取該類顯式宣告的所有類、介面、列舉組成的 Class 陣列
  4. (Class/Field/Method/Constructor).getDeclaringClass():獲取該類/屬性/方法/建構函式所在的類

Member & AccessibleObject

在講 Field、Method、Constructor 之前,先說說 Member 和 AccessibleObject。Member 是一個介面,表示 Class 的成員,前面的三個類都是其實現類。

AccessibleObject 是 Field、Method、Constructor 三個類共同繼承的父類,它提供了將反射的物件標記為在使用時取消預設 Java 語言訪問控制檢查的能力。通過 setAccessible 方法可以忽略訪問級別,從而訪問對應的內容。並且 AccessibleObject 實現了 AnnotatedElement 介面,提供了與獲取註解相關的能力。

Field

Field 提供了有關類或介面的單個屬性的資訊,以及對它的動態訪問的能力。

可以通過 Class 提供的方法,獲取 Field 物件,具體如下:

方法返回值 方法名稱 方法說明
Field getDeclaredField(String name) 獲取指定name名稱的(包含private修飾的)欄位,不包括繼承的欄位
Field[] getDeclaredField() 獲取Class物件所表示的類或介面的所有(包含private修飾的)欄位,不包括繼承的欄位
Field getField(String name) 獲取指定name名稱、具有public修飾的欄位,包含繼承欄位
Field[] getField() 獲取修飾符為public的欄位,包含繼承欄位

Field 相關的 API 我就不全域性列舉出來了,可以點選這裡檢視

Method

Method 提供了有關類或介面的單個方法的資訊,以及對它的動態訪問的能力。

可以通過 Class 提供的方法,獲取 Field 物件,具體如下:

方法返回值 方法名稱 方法說明
Method getDeclaredMethod(String name, Class<?>... parameterTypes) 返回一個指定引數的Method物件,該物件反映此 Class 物件所表示的類或介面的指定已宣告方法。
Method[] getDeclaredMethod() 返回 Method 物件的一個陣列,這些物件反映此 Class 物件表示的類或介面宣告的所有方法,包括公共、保護、預設(包)訪問和私有方法,但不包括繼承的方法。
Method getMethod(String name, Class<?>... parameterTypes) 返回一個 Method 物件,它反映此 Class 物件所表示的類或介面的指定公共成員方法。
Method[] getMethods() 返回一個包含某些 Method 物件的陣列,這些物件反映此 Class 物件所表示的類或介面(包括那些由該類或介面宣告的以及從超類和超介面繼承的那些的類或介面)的公共 member 方法。

Method 相關的 API 可以點選這裡檢視

Constructor

Constructor 提供了有關類的構造方法的資訊,以及對它的動態訪問的能力。

可以通過 Class 提供的方法,獲取 Constructor 物件,具體如下:

方法返回值 方法名稱 方法說明
Constructor<T> getConstructor(Class<?>... parameterTypes) 返回指定引數型別、具有public訪問許可權的建構函式物件
Constructor<?>[] getConstructors() 返回所有具有public訪問許可權的建構函式的Constructor物件陣列
Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) 返回指定引數型別、所有宣告的(包括private)建構函式物件
Constructor<?>[] getDeclaredConstructor() 返回所有宣告的(包括private)建構函式物件

Constructor 相關的 API 我就不全域性列舉出來了,可以點選這裡檢視

Array

在 Java 中陣列也是一種類,Array 提供了動態建立陣列和訪問陣列元素的靜態方法。

通過 getXXX(Object array, int index) 方法,傳入陣列物件和下標索引,可以獲取到該位置的值。

通過 newInstance(Class<?> componentType, int length) 方法,傳入陣列型別和長度,建立陣列。如下:

// 下面建立的兩個陣列等價
int x[] = new int[10];
int y[] = (int[]) Array.newInstance(int.class, 10);
// 輸出 true
System.out.println(x.length == y.length);
複製程式碼

通過 newInstance(Class<?> componentType, int... dimensions) 方法,建立多維陣列。如下:

// 下面建立的兩個陣列等價
int x[][] = new int[10][10];
int y[][] = (int[][]) Array.newInstance(int.class, 10, 10);
// 輸出 true
System.out.println(x.length == y.length);
複製程式碼

反射的應用場景

幾乎所有的 Java 框架都會使用到反射,例如動態配置:讀取寫好的配置檔案的值,然後通過反射機制將這些值設定到配置類中。總的來說應用場景有如下幾點:

  1. 框架開發中使用,例如動態配置
  2. 外掛開發中使用,例如持續整合
  3. 應用擴充套件

當然反射也有一些缺點:

  1. 效能低
  2. 可讀性差
  3. 只能在執行期間報錯

相關文章