Java核心知識體系5:反射機制詳解

2023-10-24 15:00:40

Java核心知識體系1:泛型機制詳解
Java核心知識體系2:註解機制詳解
Java核心知識體系3:異常機制詳解
Java核心知識體系4:AOP原理和切面應用

1 介紹

無論是那種語言體系,反射都是必不可少的一個技術特徵。從Java體系來說,很多常用的技術框架或多或少都使用到了反射技術,比如Spring、MyBatis、RocketMQ、FastJson 等等。反射技術強大而必要,在大多數框架中起到舉足輕重的作用。所以,反射也是Java必不可少的核心技術之一。

接下來我們來看看反射的一些技術要點:

  1. 反射的概念(即什麼是反射)?
  2. 反射的作用(它幫我們解決了哪些問題)?
  3. 反射的實現原理?
  4. 如何使用反射?
    下面我就針對以上的疑問,一一來講解。

1.1 反射是什麼?

Java反射(Reflection)是Java語言的一個核心特性,它允許執行中的Java程式碼對自身進行自我檢查,甚至修改自身的元件。具體來說,反射機制提供了在執行狀態中,對於任意一個類,都能夠了解這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性。這種動態獲取的資訊以及動態呼叫物件的方法在Java中就叫做反射。
一句話總結:反射就是在執行時才具體知曉要操作的類是什麼結構,並在執行時獲取類的完整構造,並呼叫對應的方法、屬性等。

Java的反射主要包括以下三個部分:

  • 類的載入:Java的類在需要使用時才會被載入到JVM中。這個過程是由類載入器(ClassLoader)完成的。類載入器首先檢查這個類是否已經被載入過,如果還沒有載入,那麼就會從磁碟上載入類的位元組碼並建立一個Class物件。
  • 獲取類的資訊:當一個物件被建立後,我們可以使用反射來獲取這個物件的Class物件。通過這個Class物件,我們可以獲取到這個類的所有屬性和方法。
  • 方法的呼叫:通過反射,我們可以動態的呼叫一個物件的方法。即使這個方法是一個私有的方法,也能夠通過反射來呼叫。

1.2 為什麼要用反射?

Java Reflection功能非常強大,並且非常有用,比如:

  • 獲取任意類的名稱、package資訊、所有屬性、方法、註解、型別、類載入器等
  • 獲取任意物件的屬性,並且能改變物件的屬性
  • 呼叫任意物件的方法
  • 判斷任意一個物件所屬的類
  • 範例化任意一個類的物件
  • 通過反射我們可以實現動態裝配,降低程式碼的耦合度,實現動態代理等。

具體的應用場景:

  • 框架設計:許多框架,如Spring,Hibernate等,都大量使用了反射來實現物件的自動裝配,動態代理等功能。
  • 單元測試:單元測試框架(如JUnit)會使用反射來呼叫被註解的方法。
  • 外掛化:為了實現外掛化,可以通過反射載入不同的外掛。
  • 物件序列化與反序列化:在物件進行序列化和反序列化的時候,會使用反射獲取到物件的所有屬性和方法。

2 反射的使用

在Java中,Class類與java.lang.reflect類庫配合對反射技術進行了完整的支援。在反射的Package中,我們經常使用功能類如下:

  • Constructor類表示的是Class 物件所表示的類的構造方法,利用它可以在執行時動態建立物件
  • Field類表示Class物件所表示的類的成員變數,通過它可以在執行時動態修改成員變數的屬性值(包含private)
  • Method類表示Class物件所表示的類的成員方法,通過它可以動態呼叫物件的方法(包含private)

下面將對這幾個類進行詳細介紹。

2.1 反射建立類物件

一般情況下我們通過反射建立類物件主要有兩種方式:

  • 通過 Class 物件的 newInstance() 方法

  • 通過 Constructor 物件的 newInstance() 方法

  • 通過 Class 物件的 newInstance() 方法實現

Class clz = Class.forName("com.ad.reflection.TestRefle");
TestRefle tr= (TestRefle)clz.newInstance();
  • 通過 Constructor 物件的 newInstance() 方法實現
Class clz = Class.forName("com.ad.reflection.TestRefle");
Constructor constructor = clz.getConstructor();
TestRefle tr= (TestRefle)constructor.newInstance();

這邊需要注意,通過 Constructor 物件建立類物件可以選擇特定構造方法,而通過 Class 物件則只能使用預設的無引數構造方法。
下面的程式碼演示的是通過 Constructor 呼叫有參構造方法進行了類物件初始化:

Class clz = Class.forName("com.ad.reflection.TestRefle");
Constructor constructor = clz.getConstructor(String.class);
TestRefle tr= (TestRefle)constructor.newInstance("提供一個String引數");

接下來我們繼續,通過具體的API獲取詳細的類資訊:類資訊、方法資訊、屬性資訊等。

2.2 獲取Class類物件

 // 獲取Class物件的三種方式
 根據類名: Class mailClass = MailInfo.class;
 根據物件: Class mailClass = new MailInfo().getClass();
 根據全限定類名: Class mailClass = Class.forName("com.ad.MailInfo");
 
 // 根據物件獲取資訊和範例物件
 獲取全限定類名: mailClass.getName();
 獲取類名: mailClass.getSimpleName();
 範例化: userClass.getDeclaredConstructor().newInstance();

更加詳細Class類獲取參考如下:

方法 用途
forName() (1)獲取Class物件的一個參照,但參照的類還沒有載入(該類的第一個物件沒有生成)就載入了這個類。 (2)為了產生Class參照,forName()立即就進行了初始化。
Object-getClass() 獲取Class物件的一個參照,返回表示該物件的實際型別的Class參照。
getName() 取全限定的類名(包括包名),即類的完整名字。 getSimpleName() 獲取類名(不包括包名)
getCanonicalName() 獲取全限定的類名(包括包名)
isInterface() 判斷Class物件是否是表示一個介面
getInterfaces() 返回Class物件陣列,表示Class物件所參照的類所實現的所有介面。
getSupercalss() 返回Class物件,表示Class物件所參照的類所繼承的直接基礎類別。應用該方法可在執行時發現一個物件完整的繼承結構。
newInstance() 返回一個Oject物件,是實現「虛擬構造器」的一種途徑。使用該方法建立的類,必須帶有無參的構造器。

2.3 獲取類的成員變數的資訊

Field[] fields = _class.getDeclaredFields();

更加詳細成員變數獲取參考如下:

方法 用途
getField(String name) 獲得某個公有的屬性物件
getFields() 獲取所有的公有的屬性物件
getDeclaredField(String name) 獲得某個屬性物件(public和非public)
getDeclaredFields() 獲得所有屬性物件(public和非public)

2.4 獲得類方法

Method[] methods = _class.getDeclaredMethods();

更加詳細方法獲取參考如下:

方法 用途
getMethod(String name, Class...<?> paramerterTypes) 獲得某個公有的方法物件
getMethods() 獲取所有的公有的方法物件
getDeclaredMethod(String name, Class...<?> paramerterTypes) 獲得對應類下某個方法(public和非public)
getDeclaredMethods() 獲得對應類下所有方法(public和非public)

2.5 獲得建構函式

Constructor[] constructors = _class.getDeclaredConstructors();

更加詳細建構函式獲取參考如下:

方法 用途
getConstructor(Class...<?> paramerterTypes) 獲得該類中與引數型別匹配的公有構造方法
getConstructors() 獲取該類的所有公有構造方法
getDeclaredConstructor(Class...<?> paramerterTypes) 獲得該類中與引數型別匹配的構造方法
getDeclaredConstructors() 獲取該類的所有構造方法

這樣通過反射就可以做在執行時獲取類的完整構造,並獲得類資訊了。

類名 用途
Class類 代表類的實體,在執行的Java應用程式中表示類和介面
Field類 代表類的成員變數(即類的屬性)
Method類 代表類的方法
Constructor類 代表類別建構函式

通過上面的幾個範例我們基本瞭解了反射的使用,但這僅僅是使用,我們還需深入理解反射背後的底層實現原理。

3 反射原理分析

3.1 反射的呼叫流程

1、編寫完Java專案之後,java檔案都會被編譯成一個.class檔案
2、這些class檔案在程式執行時會被ClassLoader載入到JVM中,當一個類被載入以後,JVM就會在記憶體中自動產生一個Class物件。
3、通過Class物件獲取 Field(屬性)、Method(方法)、Construcor(建構函式)
我們平時通過new的形式建立物件,本質上就是通過Class來建立個新物件

通過上面的流程我們可以看出反射的優勢:

  • 動態裝配

我們的程式在執行時,可能不一定會用到所有我們編寫和構建的類,這樣避免啟動時間太長並且浪費大量無用的機器資源。
取而代之的是動態的載入一些類,這些類可能之前用不到所以不用載入到jvm,而是在執行時根據需要才載入。

  • 降低耦合
    如果你在使用new時明確的指定類名,那這就是典型的寫死實現,而在使用反射的時候,可以只傳入類名引數,就可以生成物件,降低了耦合度,使得程式更具靈性。

完整的呼叫流程,圖片來自網上,比較模糊,後續再補一個

3.2 反射的應用場景

  • 框架設計:許多框架,如Spring,Hibernate,mybatis,dubbo,rocketmq等,都大量使用了反射來實現物件的自動裝配,動態代理等功能。
  • 單元測試:單元測試框架(如JUnit)會使用反射來呼叫被註解的方法。
  • 外掛化:為了實現外掛化,可以通過反射載入不同的外掛。
  • 物件序列化與反序列化:在物件進行序列化和反序列化的時候,會使用反射獲取到物件的所有屬性和方法。
  • 動態設定、動態代理:通過反射去讀取設定,以及代理請求

4 反射經典案例解析

以下案例來自百度文心一言大模型自動生成,已偵錯通過。

import java.lang.reflect.Method;  
  
public class ReflectionExample {  
    public static void main(String[] args) {  
        try {  
            // 獲取目標類的Class物件  
            Class<?> targetClass = Class.forName("java.util.ArrayList");  
  
            // 獲取目標類的所有公共方法  
            Method[] methods = targetClass.getMethods();  
  
            // 遍歷所有方法並列印方法名  
            for (Method method : methods) {  
                System.out.println(method.getName());  
            }  
  
            // 獲取特定方法,比如新增元素的add方法  
            Method addMethod = targetClass.getMethod("add", Object.class);  
  
            // 建立目標類的範例物件  
            Object targetObject = targetClass.newInstance();  
  
            // 呼叫add方法新增元素  
            addMethod.invoke(targetObject, "Hello, World!");  
  
            // 獲取目標類的所有屬性(欄位)並列印屬性名  
            Field[] fields = targetClass.getDeclaredFields();  
            for (Field field : fields) {  
                System.out.println(field.getName());  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

這個案例展示瞭如何使用反射來獲取目標類的Class物件,獲取並列印目標類的所有公共方法,獲取特定方法,建立目標類的範例物件,呼叫目標類的方法,以及獲取並列印目標類的所有屬性(欄位)。

總結

無論是那種語言體系(C#、Java等等),反射都是必不可少的一個技術特徵。而從Java體系來說,很多常用的技術框架或多或少都使用到了反射技術,比如Spring、MyBatis、RocketMQ、FastJson 等等。
學習好Java 反射技術能幫助你更好的理解底層呼叫的原理,也有助於設計更加 輕巧、高內聚、低耦合 的業務框架。