反射的基本原理


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

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM