反射的基本原理

YangAM發表於2018-06-24

『反射』就是指程式在執行時能夠動態的獲取到一個類的型別資訊的一種操作。它是現代框架的靈魂,幾盡所有的框架能夠提供的一些自動化機制都是靠反射實現的,這也是為什麼各類框架都不允許你覆蓋掉預設的無參構造器的原因,因為框架需要以反射機制利用無參構造器建立例項。

總的來說,『反射』是很值得大家花時間學習的,儘管大部分人都很少有機會去手寫框架,但是這將有助於你對於各類框架的理解。不奢求你通過本篇文章的學習對於『反射』能夠有多麼深層次的理解,但至少保證你瞭解『反射』的基本原理及使用。

Class 型別資訊

之間介紹過虛擬機器的類載入機制,其中我們提到過,每一種型別都會在初次使用時被載入進虛擬機器記憶體的『方法區』中,包含類中定義的屬性欄位,方法位元組碼等資訊。

Java 中使用類 java.lang.Class 來指向一個型別資訊,通過這個 Class 物件,我們就可以得到該類的所有內部資訊。而獲取一個 Class 物件的方法主要有以下三種。

類名.class

這種方式就比較簡單,只要使用類名點 class 即可得到方法區該型別的型別資訊。例如:

Object.class;
Integer.class;
int.class;
String.class;
//等等
複製程式碼

getClass 方法

Object 類有這麼一個方法:

public final native Class<?> getClass();
複製程式碼

這是一個本地方法,並且不允許子類重寫,所以理論上所有型別的例項都具有同一個 getClass 方法。具體使用上也很簡單:

Integer integer = new Integer(12);
integer.getClass();
複製程式碼

forName 方法

forName 算是獲取 Class 型別的一個最常用的方法,它允許你傳入一個全類名,該方法會返回方法區代表這個型別的 Class 物件,如果這個類還沒有被載入進方法區,forName 會先進行類載入。

public static Class<?> forName(String className) {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
複製程式碼

由於方法區 Class 型別資訊由類載入器和類全限定名唯一確定,所以想要去找這麼一個 Class 就必須提供類載入器和類全限定名,這個 forName 方法預設使用呼叫者的類載入器。

當然,Class 類中也有一個 forName 過載,允許你傳入類載入器和類全限定名來匹配方法區型別資訊。

public static Class<?> forName(String name, boolean initialize,
ClassLoader loader){
    //.....                                       
}
複製程式碼

至此,通過這些方法你可以得到任意類的型別資訊,該類的所有欄位屬性,方法表等資訊都可以通過這個 Class 物件進行獲取。

反射欄位屬性

Class 中有關獲取欄位屬性的方法主要以下幾個:

  • public Field[] getFields():返回該型別的所有 public 修飾的屬性,包括父類的
  • public Field getField(String name):根據欄位名稱返回相應的欄位
  • public Field[] getDeclaredFields():返回本型別中申明的所有欄位,包含非 public 修飾的但不包含父類中的
  • public Field getDeclaredField(String name):同理

當然,一個 Field 例項包含某個類的一個屬性的所有資訊,包括欄位名稱,訪問修飾符,欄位型別。除此之外,Field 還提供了大量的操作該屬性值的方法,通過傳入一個類例項,就可以直接使用 Field 例項操作該例項的當前欄位屬性的值。

例如:

//定義一個待反射類
public class People {
    public String name;
}
複製程式碼
Class<People> cls = People.class;
Field name = cls.getField("name");
People people = new People();
name.set(people,"hello");
System.out.println(people.name);
複製程式碼

程式會輸出:

hello
複製程式碼

其實也很簡單,set 方法會檢索 People 物件是否具有一個 name 代表的欄位,如果有將字串 hello 賦值給該欄位即可。

整個 Field 類主要由兩大部分組成,第一部分就是有關該欄位屬性的描述資訊,例如名稱,型別,外圍類 Class 物件等,第二部分就是大量的 get 和 set 方法用於間接操作任意的外圍類例項的當前屬性值。

反射方法

同樣的,Class 類也提供了四種方法來獲取其中的方法屬性:

  • public Method[] getMethods():返回所有的 public 方法,包括父類中的
  • public Method getMethod(String name, Class<?>... parameterTypes):返回指定的方法
  • public Method[] getDeclaredMethods():返回本類申明的所有方法,包括非 public 修飾的,但不包括父類中的
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes):同理

Method 抽象地代表了一個方法,同樣有描述這個方法基本資訊的欄位和方法,例如方法名,方法的引數集合,方法的返回值型別,異常型別集合,方法的註解等。

除此之外的還有一個 invoke 方法用於間接呼叫其他例項的該方法,例如:

public class People {

    public void sayHello(){
        System.out.println("hello wrold ");
    }
}
複製程式碼
Class<People> cls = People.class;
Method sayHello = cls.getMethod("sayHello");
People people = new People();
sayHello.invoke(people);
複製程式碼

程式輸出:

hello wrold
複製程式碼

反射構造器

對於 Constructor 來說,Class 類依然為它提供了四種獲取例項的方法:

  • public Constructor<?>[] getConstructors():返回所有 public 修飾的構造器
  • public Constructor<?>[] getDeclaredConstructors():返回所有的構造器,無視訪問修飾符
  • public Constructor getConstructor(Class<?>... parameterTypes):帶指定引數的
  • public Constructor getDeclaredConstructor(Class<?>... parameterTypes) :同理

Constructor 本質上也是一個方法,只是沒有返回值而已,所以內部的基本內容和 Method 是類似的,只不過 Constructor 類中有一個 newInstance 方法用於建立一個該 Class 型別的例項物件出來。

//最簡單的一個反射建立例項的過程
Class<People> cls = People.class;
Constructor c = cls.getConstructor();
People p = (People) c.newInstance();
複製程式碼

以上,我們簡單的介紹了反射的基本使用情況,但都很基礎,下面我們看看反射和一些稍微複雜的型別結合使用的情況,例如:陣列,泛型,註解等。

反射的其他細節

反射與陣列

我們都知道,陣列是一種特殊的型別,它本質上由虛擬機器在執行時動態生成,所以在反射這種型別的時候會稍有不同。

public native Class<?> getComponentType();
複製程式碼

Class 中有這麼一個方法,該方法將返回陣列 Class 例項元素的基本型別。只有當前的 Class 物件代表的是一個陣列型別的時候,該方法才會返回陣列的元素實際型別,其他的任何時候都會返回 null。

當然,有一點需要注意下,代表陣列的這個由虛擬機器動態建立的型別,它直接繼承的 Object 類,並且所有有關陣列類的操作,比如為某個元素賦值或是獲取陣列長度的操作都直接對應一個單獨的虛擬機器陣列操作指令。

同樣也因為陣列類直接由虛擬機器執行時動態建立,所以你不可能從一個陣列型別的 Class 例項中得到構造方法,編譯器根本沒機會為類生成預設的構造器。於是你也不能以常規的方法通過 Constructor 來建立一個該類的例項物件。

如果你非要嘗試使用 Constructor 來建立一個新的例項的話,那麼執行時程式將告訴你無法匹配一個構造器。像這樣:

Class<String[]> cls = String[].class;
Constructor constructor = cls.getConstructor();
String[] strs = (String[]) constructor.newInstance();
複製程式碼

控制檯輸出:

image

告訴你,Class 例項中根本找不到一個無參的構造器。那麼難道我們就沒有辦法來動態建立一個陣列了嗎?

當然不是,Java 中有一個類 java.lang.reflect.Array 提供了一些靜態的方法用於動態的建立和獲取一個陣列型別。

//建立一個一維陣列,componentType 為陣列元素型別,length 陣列長度
public static Object newInstance(Class<?> componentType, int length)

//可變引數 dimensions,指定多個維度的單維度長度
public static Object newInstance(Class<?> componentType, int... dimensions)
複製程式碼

這是我認為 Array 類中最重要的兩個方法,當然了 Array 類中還有一些其它方法用於獲取指定陣列的指定位置元素,這裡不再贅述了。

完全是因為陣列這種型別並不是由常規的編譯器編譯生成,而是由虛擬機器動態建立的,所以想要通過反射的方式例項化一個陣列型別是得依賴 Array 這個型別的相關 newInstance 方法的。

反射與泛型

泛型是 Java 編譯器範圍內的概念,它能夠在程式執行之前提供一定的安全檢查,而反射是執行時發生的,也就是說如果你反射呼叫一個泛型方法,實際上就繞過了編譯器的泛型檢查了。我們看一段程式碼:

ArrayList<Integer> list = new ArrayList<>();
list.add(23);
//list.add("fads");編譯不通過

Class<?> cls = list.getClass();
Method add = cls.getMethod("add",Object.class);
add.invoke(list,"hello");
System.out.println(list.get(1));
複製程式碼

最終你會發現我們從整型容器中取出一個字串,因為虛擬機器只管在執行時從方法區找到 ArrayList 這個類的型別資訊並解析出它的 add 方法,接著執行這個方法。

它不像一般的方法呼叫,呼叫之前編譯器會檢測這個方法存在不存在,引數型別是否匹配等,所以沒了編譯器的這層安全檢查,反射地呼叫方法更容易遇到問題。

除此之外,之前我們說過的泛型在經過編譯期之後會被型別擦除,但實際上代表該型別的 Class 型別資訊中是儲存有一些基本的泛型資訊的,這一點我們可以通過反射得到。

這裡不再帶大家一起去看了,Class ,Field 和 Method 中都是有相關方法可以獲取類或者方法在定義的時候所使用到的泛型類名名稱。注意這裡說的,只是名稱,類似 E、V 這樣的東西。


文章中的所有程式碼、圖片、檔案都雲端儲存在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:OneJavaCoder,所有文章都將同步在公眾號上。

image

相關文章