? 盡人事,聽天命。博主東南大學碩士在讀,熱愛健身和籃球,樂於分享技術相關的所見所得,關注公眾號 @ 飛天小牛肉,第一時間獲取文章更新,成長的路上我們一起進步
? 本文已收錄於 「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 不會再生成一個 Date
的 Class
物件,因為已經存在一個了。這也使得我們可以利用 ==
運算子實現兩個類物件比較的操作:
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
是無法滿足的。
此時,我們可以使用 Constructor
的 newInstance
方法來實現,先獲取建構函式,再執行建構函式。
從上面程式碼很容易看出,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
- 《Java 核心技術 - 卷 1 基礎知識 - 第 10 版》
- 《Thinking In Java(Java 程式設計思想)- 第 4 版》
- 敬業的小馬哥 — Java 基礎之反射:https://blog.csdn.net/sinat_38259539/article/details/71799078
? 關注公眾號 | 飛天小牛肉,即時獲取更新
- 博主東南大學碩士在讀,利用課餘時間運營一個公眾號『 飛天小牛肉 』,2020/12/29 日開通,專注分享計算機基礎(資料結構 + 演算法 + 計算機網路 + 資料庫 + 作業系統 + Linux)、Java 基礎和麵試指南的相關原創技術好文。本公眾號的目的就是讓大家可以快速掌握重點知識,有的放矢。希望大家多多支援哦,和小牛肉一起成長 ?
- 並推薦個人維護的開源教程類專案: CS-Wiki(Gitee 推薦專案,現已累計 1.5k+ star), 致力打造完善的後端知識體系,在技術的路上少走彎路,歡迎各位小夥伴前來交流學習 ~ ?
- 如果各位小夥伴春招秋招沒有拿得出手的專案的話,可以參考我寫的一個專案「開源社群系統 Echo」Gitee 官方推薦專案,目前已累計 330+ star,基於 SpringBoot + MyBatis + MySQL + Redis + Kafka + Elasticsearch + Spring Security + ... 並提供詳細的開發文件和配套教程。公眾號後臺回覆 Echo 可以獲取配套教程,目前尚在更新中。