讀懂框架設計的靈魂—Java反射機制


🎓 盡人事,聽天命。博主東南大學碩士在讀,熱愛健身和籃球,樂於分享技術相關的所見所得,關注公眾號 @ 飛天小牛肉,第一時間獲取文章更新,成長的路上我們一起進步

🎁 本文已收錄於 「CS-Wiki」Gitee 官方推薦項目,現已累計 1.5k+ star,致力打造完善的后端知識體系,在技術的路上少走彎路,歡迎各位小伙伴前來交流學習

🍉 如果各位小伙伴春招秋招沒有拿得出手的項目的話,可以參考我寫的一個項目「開源社區系統 Echo」Gitee 官方推薦項目,目前已累計 330+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文檔和配套教程。公眾號后台回復 Echo 可以獲取配套教程,目前尚在更新中


Java 反射機制對於小白來說,真的是一道巨大的坎兒,其他的東西吧,無非就是內容多點,多看看多背背就好了,反射真的就是不管看了多少遍不理解就還是不理解,而且學校里面的各大教材應該都沒有反射這個章節,有也是一帶而過。說實話,在這篇文章之前,我對反射也並非完全了解,畢竟平常開發基本用不到,不過,看完這篇文章相信你對反射就沒啥疑點了。

全文脈絡思維導圖如下:

1. 拋磚引玉:為什么要使用反射

前文我們說過,接口的使用提高了代碼的可維護性和可擴展性,並且降低了代碼的耦合度。來看個例子:

首先,我們擁有一個接口 X 及其方法 test,和兩個對應的實現類 A、B:

public class Test {
    
    interface X {
    	public void test();
	}

    class A implements X{
        @Override
        public void test() {
             System.out.println("I am A");
        }
    }

    class B implements X{
        @Override
        public void test() {
            System.out.println("I am B");
    }
}

通常情況下,我們需要使用哪個實現類就直接 new 一個就好了,看下面這段代碼:

public class Test {    

    ......

	public static void main(String[] args) {
        X a = create1("A");
        a.test();
        X b = create1("B");
        b.test();
    }

    public static X create1(String name){
        if (name.equals("A")) {
            return new A();
        } else if(name.equals("B")){
            return new B();
        }
        return null;
    }

}

按照上面這種寫法,如果有成百上千個不同的 X 的實現類需要創建,那我們豈不是就需要寫上千個 if 語句來返回不同的 X 對象?

我們來看看看反射機制是如何做的:

public class Test {

    public static void main(String[] args) {
		X a = create2("A");
        a.test();
        X b = create2("B");
        b.testReflect();
    }
    
	// 使用反射機制
    public static X create2(String name){
        Class<?> class = Class.forName(name);
        X x = (X) class.newInstance();
        return x;
    }
}

create2() 方法傳入包名和類名,通過反射機制動態的加載指定的類,然后再實例化對象。

看完上面這個例子,相信諸位對反射有了一定的認識。反射擁有以下四大功能:

  • 在運行時(動態編譯)獲知任意一個對象所屬的類。
  • 在運行時構造任意一個類的對象。
  • 在運行時獲知任意一個類所具有的成員變量和方法。
  • 在運行時調用任意一個對象的方法和屬性。

上述這種動態獲取信息、動態調用對象的方法的功能稱為 Java 語言的反射機制。

2. 理解 Class 類

要想理解反射,首先要理解 Class 類,因為 Class 類是反射實現的基礎。

在程序運行期間,JVM 始終為所有的對象維護一個被稱為運行時的類型標識,這個信息跟蹤着每個對象所屬的類的完整結構信息,包括包名、類名、實現的接口、擁有的方法和字段等。可以通過專門的 Java 類訪問這些信息,這個類就是 Class 類。我們可以把 Class 類理解為類的類型,一個 Class 對象,稱為類的類型對象,一個 Class 對象對應一個加載到 JVM 中的一個 .class 文件

在通常情況下,一定是先有類再有對象。以下面這段代碼為例,類的正常加載過程是這樣的:

import java.util.Date; // 先有類

public class Test {
    public static void main(String[] args) {
        Date date = new Date(); // 后有對象
        System.out.println(date);
    }
}

首先 JVM 會將你的代碼編譯成一個 .class 字節碼文件,然后被類加載器(Class Loader)加載進 JVM 的內存中,同時會創建一個 Date 類的 Class 對象存到堆中(注意這個不是 new 出來的對象,而是類的類型對象)。JVM 在創建 Date 對象前,會先檢查其類是否加載,尋找類對應的 Class 對象,若加載好,則為其分配內存,然后再進行初始化 new Date()

需要注意的是,每個類只有一個 Class 對象,也就是說如果我們有第二條 new Date() 語句,JVM 不會再生成一個 DateClass 對象,因為已經存在一個了。這也使得我們可以利用 == 運算符實現兩個類對象比較的操作:

System.out.println(date.getClass() == Date.getClass()); // true

OK,那么在加載完一個類后,堆內存的方法區就產生了一個 Class 對象,這個對象就包含了完整的類的結構信息,我們可以通過這個 Class 對象看到類的結構,就好比一面鏡子。所以我們形象的稱之為:反射。

說的再詳細點,再解釋一下。上文說過,在通常情況下,一定是先有類再有對象,我們把這個通常情況稱為 “正”。那么反射中的這個 “反” 我們就可以理解為根據對象找到對象所屬的類(對象的出處)

Date date = new Date();
System.out.println(date.getClass()); // "class java.util.Date"

通過反射,也就是調用了 getClass() 方法后,我們就獲得了 Date 類對應的 Class 對象,看到了 Date 類的結構,輸出了 Date 對象所屬的類的完整名稱,即找到了對象的出處。當然,獲取 Class 對象的方式不止這一種。

3. 獲取 Class 類對象的四種方式

Class 類的源碼可以看出,它的構造函數是私有的,也就是說只有 JVM 可以創建 Class 類的對象,我們不能像普通類一樣直接 new 一個 Class 對象。

我們只能通過已有的類來得到一個 Class 類對象,Java 提供了四種方式:

第一種:知道具體類的情況下可以使用

Class alunbarClass = TargetObject.class;

但是我們一般是不知道具體類的,基本都是通過遍歷包下面的類來獲取 Class 對象,通過此方式獲取 Class 對象不會進行初始化。

第二種:通過 Class.forName() 傳入全類名獲取

Class alunbarClass1 = Class.forName("com.xxx.TargetObject");

這個方法內部實際調用的是 forName0

第 2 個 boolean 參數表示類是否需要初始化,默認是需要初始化。一旦初始化,就會觸發目標對象的 static 塊代碼執行,static 參數也會被再次初始化。

第三種:通過對象實例 instance.getClass() 獲取

Date date = new Date();
Class alunbarClass2 = date.getClass(); // 獲取該對象實例的 Class 類對象

第四種:通過類加載器 xxxClassLoader.loadClass() 傳入類路徑獲取

class clazz = ClassLoader.LoadClass("com.xxx.TargetObject");

通過類加載器獲取 Class 對象不會進行初始化,意味着不進行包括初始化等一些列步驟,靜態塊和靜態對象不會得到執行。這里可以和 forName 做個對比。

4. 通過反射構造一個類的實例

上面我們介紹了獲取 Class 類對象的方式,那么成功獲取之后,我們就需要構造對應類的實例。下面介紹三種方法,第一種最為常見,最后一種大家稍作了解即可。

① 使用 Class.newInstance

舉個例子:

Date date1 = new Date();
Class alunbarClass2 = date1.getClass();
Date date2 = alunbarClass2.newInstance(); // 創建一個與 alunbarClass2 具有相同類類型的實例

創建了一個與 alunbarClass2 具有相同類類型的實例。

需要注意的是,newInstance 方法調用默認的構造函數(無參構造函數)初始化新創建的對象。如果這個類沒有默認的構造函數, 就會拋出一個異常

② 通過反射先獲取構造方法再調用

由於不是所有的類都有無參構造函數又或者類構造器是 private 的,在這樣的情況下,如果我們還想通過反射來實例化對象,Class.newInstance 是無法滿足的。

此時,我們可以使用 ConstructornewInstance 方法來實現,先獲取構造函數,再執行構造函數。

從上面代碼很容易看出,Constructor.newInstance 是可以攜帶參數的,而 Class.newInstance 是無參的,這也就是為什么它只能調用無參構造函數的原因了。

大家不要把這兩個 newInstance 方法弄混了。如果被調用的類的構造函數為默認的構造函數,采用Class.newInstance() 是比較好的選擇, 一句代碼就 OK;如果需要調用類的帶參構造函數、私有構造函數等, 就需要采用 Constractor.newInstance()

Constructor.newInstance 是執行構造函數的方法。我們來看看獲取構造函數可以通過哪些渠道,作用如其名,以下幾個方法都比較好記也容易理解,返回值都通過 Cnostructor 類型來接收。

批量獲取構造函數

1)獲取所有"公有的"構造方法

public Constructor[] getConstructors() { }

2)獲取所有的構造方法(包括私有、受保護、默認、公有)

public Constructor[] getDeclaredConstructors() { }

單個獲取構造函數

1)獲取一個指定參數類型的"公有的"構造方法

public Constructor getConstructor(Class... parameterTypes) { }

2)獲取一個指定參數類型的"構造方法",可以是私有的,或受保護、默認、公有

public Constructor getDeclaredConstructor(Class... parameterTypes) { }

舉個例子:

package fanshe;

public class Student {
	//(默認的構造方法)
	Student(String str){
		System.out.println("(默認)的構造方法 s = " + str);
	}
	// 無參構造方法
	public Student(){
		System.out.println("調用了公有、無參構造方法執行了。。。");
	}
	// 有一個參數的構造方法
	public Student(char name){
		System.out.println("姓名:" + name);
	}
	// 有多個參數的構造方法
	public Student(String name ,int age){
		System.out.println("姓名:"+name+"年齡:"+ age);//這的執行效率有問題,以后解決。
	}
	// 受保護的構造方法
	protected Student(boolean n){
		System.out.println("受保護的構造方法 n = " + n);
	}
	// 私有構造方法
	private Student(int age){
		System.out.println("私有的構造方法年齡:"+ age);
	}
}

----------------------------------
    
public class Constructors {
	public static void main(String[] args) throws Exception {
		// 加載Class對象
		Class clazz = Class.forName("fanshe.Student");
        
		// 獲取所有公有構造方法
		Constructor[] conArray = clazz.getConstructors();
		for(Constructor c : conArray){
			System.out.println(c);
		}
        
		// 獲取所有的構造方法(包括:私有、受保護、默認、公有)
		conArray = clazz.getDeclaredConstructors();
		for(Constructor c : conArray){
			System.out.println(c);
		}
        
		// 獲取公有、無參的構造方法
        // 因為是無參的構造方法所以類型是一個null,不寫也可以:這里需要的是一個參數的類型,切記是類型
		// 返回的是描述這個無參構造函數的類對象。
		Constructor con = clazz.getConstructor(null);
		Object obj = con.newInstance(); // 調用構造方法
		
		// 獲取私有構造方法
		con = clazz.getDeclaredConstructor(int.class);
		System.out.println(con);
		con.setAccessible(true); // 為了調用 private 方法/域 我們需要取消安全檢查
		obj = con.newInstance(12); // 調用構造方法
	}
}

③ 使用開源庫 Objenesis

Objenesis 是一個開源庫,和上述第二種方法一樣,可以調用任意的構造函數,不過封裝的比較簡潔:

public class Test {
    // 不存在無參構造函數
    private int i;
    public Test(int i){
        this.i = i;
    }
    public void show(){
        System.out.println("test..." + i);
    }
}

------------------------
    
public static void main(String[] args) {
        Objenesis objenesis = new ObjenesisStd(true);
        Test test = objenesis.newInstance(Test.class);
        test.show();
    }

使用非常簡單,Objenesis 由子類 ObjenesisObjenesisStd實現。詳細源碼此處就不深究了,了解即可。

5. 通過反射獲取成員變量並使用

和獲取構造函數差不多,獲取成員變量也分批量獲取和單個獲取。返回值通過 Field 類型來接收。

批量獲取

1)獲取所有公有的字段

public Field[] getFields() { }

2)獲取所有的字段(包括私有、受保護、默認的)

public Field[] getDeclaredFields() { }

單個獲取

1)獲取一個指定名稱的公有的字段

public Field getField(String name) { }

2)獲取一個指定名稱的字段,可以是私有、受保護、默認的

public Field getDeclaredField(String name) { }

獲取到成員變量之后,如何修改它們的值呢?

set 方法包含兩個參數:

  • obj:哪個對象要修改這個成員變量
  • value:要修改成哪個值

舉個例子:

package fanshe.field;

public class Student {
	public Student(){
        
	}
	
	public String name;
	protected int age;
	char sex;
	private String phoneNum;
	
	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + ", sex=" + sex
				+ ", phoneNum=" + phoneNum + "]";
	}
}

----------------------------------
    
public class Fields {
    public static void main(String[] args) throws Exception {
        // 獲取 Class 對象
        Class stuClass = Class.forName("fanshe.field.Student");
        // 獲取公有的無參構造函數
        Constructor con = stuClass.getConstructor();
		
		// 獲取私有構造方法
		con = clazz.getDeclaredConstructor(int.class);
		System.out.println(con);
		con.setAccessible(true); // 為了調用 private 方法/域 我們需要取消安全檢查
		obj = con.newInstance(12); // 調用構造方法
        
        // 獲取所有公有的字段
        Field[] fieldArray = stuClass.getFields();
        for(Field f : fieldArray){
            System.out.println(f);
        }

         // 獲取所有的字段 (包括私有、受保護、默認的)
        fieldArray = stuClass.getDeclaredFields();
        for(Field f : fieldArray){
            System.out.println(f);
        }

        // 獲取指定名稱的公有字段
        Field f = stuClass.getField("name");
        Object obj = con.newInstance(); // 調用構造函數,創建該類的實例
        f.set(obj, "劉德華"); // 為 Student 對象中的 name 屬性賦值


        // 獲取私有字段
        f = stuClass.getDeclaredField("phoneNum");
        f.setAccessible(true); // 暴力反射,解除私有限定
        f.set(obj, "18888889999"); // 為 Student 對象中的 phoneNum 屬性賦值
    }
}

6. 通過反射獲取成員方法並調用

同樣的,獲取成員方法也分批量獲取和單個獲取。返回值通過 Method 類型來接收。

批量獲取

1)獲取所有"公有方法"(包含父類的方法,當然也包含 Object 類)

public Method[] getMethods() { }

2)獲取所有的成員方法,包括私有的(不包括繼承的)

public Method[] getDeclaredMethods() { }

單個獲取

獲取一個指定方法名和參數類型的成員方法:

public Method getMethod(String name, Class<?>... parameterTypes)

獲取到方法之后該怎么調用它們呢?

invoke 方法中包含兩個參數:

  • obj:哪個對象要來調用這個方法
  • args:調用方法時所傳遞的實參

舉個例子:

package fanshe.method;
 
public class Student {
	public void show1(String s){
		System.out.println("調用了:公有的,String參數的show1(): s = " + s);
	}
	protected void show2(){
		System.out.println("調用了:受保護的,無參的show2()");
	}
	void show3(){
		System.out.println("調用了:默認的,無參的show3()");
	}
	private String show4(int age){
		System.out.println("調用了,私有的,並且有返回值的,int參數的show4(): age = " + age);
		return "abcd";
	}
}

-------------------------------------------
public class MethodClass {
	public static void main(String[] args) throws Exception {
		// 獲取 Class對象
		Class stuClass = Class.forName("fanshe.method.Student");
        // 獲取公有的無參構造函數
        Constructor con = stuClass.getConstructor();
        
		// 獲取所有公有方法
		stuClass.getMethods();
		Method[] methodArray = stuClass.getMethods();
		for(Method m : methodArray){
			System.out.println(m);
		}
        
		// 獲取所有的方法,包括私有的
		methodArray = stuClass.getDeclaredMethods();
		for(Method m : methodArray){
			System.out.println(m);
		}
        
		// 獲取公有的show1()方法
		Method m = stuClass.getMethod("show1", String.class);
		System.out.println(m);
		Object obj = con.newInstance(); // 調用構造函數,實例化一個 Student 對象
		m.invoke(obj, "小牛肉");
		
		// 獲取私有的show4()方法
		m = stuClass.getDeclaredMethod("show4", int.class);
		m.setAccessible(true); // 解除私有限定
		Object result = m.invoke(obj, 20);
		System.out.println("返回值:" + result);
	}
}

7. 反射機制優缺點

優點: 比較靈活,能夠在運行時動態獲取類的實例。

缺點

1)性能瓶頸:反射相當於一系列解釋操作,通知 JVM 要做的事情,性能比直接的 Java 代碼要慢很多。

2)安全問題:反射機制破壞了封裝性,因為通過反射可以獲取並調用類的私有方法和字段。

8. 反射的經典應用場景

反射在我們實際編程中其實並不會直接大量的使用,但是實際上有很多設計都與反射機制有關,比如:

  • 動態代理機制
  • 使用 JDBC 連接數據庫
  • Spring / Hibernate 框架(實際上是因為使用了動態代理,所以才和反射機制有關)

為什么說動態代理使用了反射機制,下篇文章會給出詳細解釋。

JDBC 連接數據庫

在 JDBC 的操作中,如果要想進行數據庫的連接,則必須按照以下幾步完成:

  • 通過 Class.forName() 加載數據庫的驅動程序 (通過反射加載)
  • 通過 DriverManager 類連接數據庫,參數包含數據庫的連接地址、用戶名、密碼
  • 通過 Connection 接口接收連接
  • 關閉連接
public static void main(String[] args) throws Exception {  
        Connection con = null; // 數據庫的連接對象  
        // 1. 通過反射加載驅動程序
        Class.forName("com.mysql.jdbc.Driver"); 
        // 2. 連接數據庫  
        con = DriverManager.getConnection(
            "jdbc:mysql://localhost:3306/test","root","root"); 
        // 3. 關閉數據庫連接
        con.close(); 
}

Spring 框架

反射機制是 Java 框架設計的靈魂,框架的內部都已經封裝好了,我們自己基本用不着寫。典型的除了Hibernate 之外,還有 Spring 也用到了很多反射機制,最典型的就是 Spring 通過 xml 配置文件裝載 Bean(創建對象),也就是 Spring 的 IoC,過程如下:

  • 加載配置文件,獲取 Spring 容器
  • 使用反射機制,根據傳入的字符串獲得某個類的 Class 實例
// 獲取 Spring 的 IoC 容器,並根據 id 獲取對象
public static void main(String[] args) {
    // 1.使用 ApplicationContext 接口加載配置文件,獲取 spring 容器
    ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
    // 2. 使用反射機制,根據這個字符串獲得某個類的 Class 實例
    IAccountService aService = (IAccountService) ac.getBean("accountServiceImpl");
    System.out.println(aService);
}

另外,Spring AOP 由於使用了動態代理,所以也使用了反射機制,這點我會在 Spring 的系列文章中詳細解釋。

📚 References

🎉 關注公眾號 | 飛天小牛肉,即時獲取更新

  • 博主東南大學碩士在讀,利用課余時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(數據結構 + 算法 + 計算機網絡 + 數據庫 + 操作系統 + Linux)、Java 基礎和面試指南的相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。希望大家多多支持哦,和小牛肉一起成長 😃
  • 並推薦個人維護的開源教程類項目: CS-Wiki(Gitee 推薦項目,現已累計 1.5k+ star), 致力打造完善的后端知識體系,在技術的路上少走彎路,歡迎各位小伙伴前來交流學習 ~ 😊
  • 如果各位小伙伴春招秋招沒有拿得出手的項目的話,可以參考我寫的一個項目「開源社區系統 Echo」Gitee 官方推薦項目,目前已累計 330+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文檔和配套教程。公眾號后台回復 Echo 可以獲取配套教程,目前尚在更新中。


免責聲明!

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



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