Java 反射
本小節(jié)我們來學(xué)習(xí)一個(gè) Java 語(yǔ)言中較為深入的概念 —— 反射(reflection),很多小伙伴即便參與了工作,可能也極少用到 Java 反射機(jī)制,但是如果你想要開發(fā)一個(gè) web 框架,反射是不可或缺的知識(shí)點(diǎn)。本小節(jié)我們將了解到 什么是反射,反射的使用場(chǎng)景,不得不提的 Class
類,如何通過反射訪問類內(nèi)部的字段、方法以及構(gòu)造方法等知識(shí)點(diǎn)。
1. 什么是反射
Java 的反射(reflection)機(jī)制是指在程序的運(yùn)行狀態(tài)中,可以構(gòu)造任意一個(gè)類的對(duì)象,可以了解任意一個(gè)對(duì)象所屬的類,可以了解任意一個(gè)類的成員變量和方法,可以調(diào)用任意一個(gè)對(duì)象的屬性和方法。這種動(dòng)態(tài)獲取程序信息以及動(dòng)態(tài)調(diào)用對(duì)象的功能稱為 Java 語(yǔ)言的反射機(jī)制。反射被視為動(dòng)態(tài)語(yǔ)言的關(guān)鍵。
通常情況下,我們想調(diào)用一個(gè)類內(nèi)部的屬性或方法,需要先實(shí)例化這個(gè)類,然后通過對(duì)象去調(diào)用類內(nèi)部的屬性和方法;通過 Java 的反射機(jī)制,我們就可以在程序的運(yùn)行狀態(tài)中,動(dòng)態(tài)獲取類的信息,注入類內(nèi)部的屬性和方法,完成對(duì)象的實(shí)例化等操作。
概念可能比較抽象,我們來看一下結(jié)合示意圖看一下:

圖中解釋了兩個(gè)問題:
- 程序運(yùn)行狀態(tài)中指的是什么時(shí)刻:
Hello.java
源代碼文件經(jīng)過編譯得到Hello.class
字節(jié)碼文件,想要運(yùn)行這個(gè)程序,就要通過 JVM 的 ClassLoader (類加載器)加載Hello.class
,然后 JVM 來運(yùn)行 Hello.class,程序的運(yùn)行期間指的就是此刻; - 什么是反射,它有哪些功能:在程序運(yùn)行期間,可以動(dòng)態(tài)獲得
Hello
類中的屬性和方法、動(dòng)態(tài)完成Hello
類的對(duì)象實(shí)例化等操作,這個(gè)功能就稱為反射。
說到這里,大家可能覺得,在編寫代碼時(shí)直接通過 new
的方式就可以實(shí)例化一個(gè)對(duì)象,訪問其屬性和方法,為什么偏偏要繞個(gè)彎子,通過反射機(jī)制來進(jìn)行這些操作呢?下面我們就來看一下反射的使用場(chǎng)景。
2. 反射的使用場(chǎng)景
Java 的反射機(jī)制,主要用來編寫一些通用性較高的代碼或者編寫框架的時(shí)候使用。
通過反射的概念,我們可以知道,在程序的運(yùn)行狀態(tài)中,對(duì)于任意一個(gè)類,通過反射都可以動(dòng)態(tài)獲取其信息以及動(dòng)態(tài)調(diào)用對(duì)象。
例如,很多框架都可以通過配置文件,來讓開發(fā)者指定使用不同的類,開發(fā)者只需要關(guān)心配置,不需要關(guān)心代碼的具體實(shí)現(xiàn),具體實(shí)現(xiàn)都在框架的內(nèi)部,通過反射就可以動(dòng)態(tài)生成類的對(duì)象,調(diào)用這個(gè)類下面的一些方法。
下面的內(nèi)容,我們將學(xué)習(xí)反射的相關(guān) API
,在本小節(jié)的最后,我將分享一個(gè)自己實(shí)際開發(fā)中的反射案例。
3. 反射常用類概述
學(xué)習(xí)反射就需要了解反射相關(guān)的一些類,下面我們來看一下如下這幾個(gè)類:
- Class:
Class
類的實(shí)例表示正在運(yùn)行的Java
應(yīng)用程序中的類和接口; - Constructor:關(guān)于類的單個(gè)構(gòu)造方法的信息以及對(duì)它的權(quán)限訪問;
- Field:Field 提供有關(guān)類或接口的單個(gè)字段的信息,以及對(duì)它的動(dòng)態(tài)訪問權(quán)限;
- Method:Method 提供關(guān)于類或接口上單獨(dú)某個(gè)方法的信息。
字節(jié)碼文件想要運(yùn)行都是要被虛擬機(jī)加載的,每加載一種類,Java 虛擬機(jī)都會(huì)為其創(chuàng)建一個(gè) Class
類型的實(shí)例,并關(guān)聯(lián)起來。
例如,我們自定義了一個(gè) ImoocStudent.java
類,類中包含有構(gòu)造方法、成員屬性、成員方法等:
public class ImoocStudent {
// 無參構(gòu)造方法
public ImoocStudent() {
}
// 有參構(gòu)造方法
public ImoocStudent(String nickname) {
this.nickname = nickname;
}
// 昵稱
private String nickname;
// 定義getter和setter方法
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
}
源碼文件 ImoocStudent.java
會(huì)被編譯器編譯成字節(jié)碼文件 ImoocStudent.class
,當(dāng) Java 虛擬機(jī)加載這個(gè) ImoocStudent.class
的時(shí)候,就會(huì)創(chuàng)建一個(gè) Class
類型的實(shí)例對(duì)象:
Class cls = new Class(ImoocStudent);
JVM 為我們自動(dòng)創(chuàng)建了這個(gè)類的對(duì)象實(shí)例,因此就可以獲取類內(nèi)部的構(gòu)造方法、屬性和方法等 ImoocStudent
的構(gòu)造方法就稱為 Constructor
,可以創(chuàng)建對(duì)象的實(shí)例,屬性就稱為 Field
,可以為屬性賦值,方法就稱為 Method
,可以執(zhí)行方法。
4. Class 類
4.1 Class 類和 class 文件的關(guān)系
java.lang.Class
類用于表示一個(gè)類的字節(jié)碼(.class)文件。
4.2 獲取 Class 對(duì)象的方法
想要使用反射,就要獲取某個(gè) class
文件對(duì)應(yīng)的 Class
對(duì)象,我們有 3 種方法:
- 類名.class:即通過一個(gè)
Class
的靜態(tài)變量class
獲取,實(shí)例如下:
Class cls = ImoocStudent.class;
- 對(duì)象.getClass ():前提是有該類的對(duì)象實(shí)例,該方法由
java.lang.Object
類提供,實(shí)例如下:
ImoocStudent imoocStudent = new ImoocStudent("小慕");
Class imoocStudent.getClass();
- Class.forName (“包名。類名”):如果知道一個(gè)類的完整包名,可以通過
Class
類的靜態(tài)方法forName()
獲得Class
對(duì)象,實(shí)例如下:
class cls = Class.forName("java.util.ArrayList");
4.3 實(shí)例
package com.imooc.reflect;
public class ImoocStudent {
// 無參構(gòu)造方法
public ImoocStudent() {
}
// 有參構(gòu)造方法
public ImoocStudent(String nickname) {
this.nickname = nickname;
}
// 昵稱
private String nickname;
// 定義getter和setter方法
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public static void main(String[] args) throws ClassNotFoundException {
// 方法1:類名.class
Class cls1 = ImoocStudent.class;
// 方法2:對(duì)象.getClass()
ImoocStudent student = new ImoocStudent();
Class cls2 = student.getClass();
// 方法3:Class.forName("包名.類名")
Class cls3 = Class.forName("com.imooc.reflect.ImoocStudent");
}
}
代碼中,我們?cè)?com.imooc.reflect
包下定義了一個(gè) ImoocStudent
類,并在主方法中,使用了 3 種方法獲取 Class
的實(shí)例對(duì)象,其 forName()
方法會(huì)拋出一個(gè) ClassNotFoundException
。
4.4 調(diào)用構(gòu)造方法
獲取了 Class
的實(shí)例對(duì)象,我們就可以獲取 Contructor
對(duì)象,調(diào)用其構(gòu)造方法了。
那么如何獲得 Constructor
對(duì)象?Class
提供了以下幾個(gè)方法來獲取:
Constructor getConstructor(Class...)
:獲取某個(gè)public
的構(gòu)造方法;Constructor getDeclaredConstructor(Class...)
:獲取某個(gè)構(gòu)造方法;Constructor[] getConstructors()
:獲取所有public
的構(gòu)造方法;Constructor[] getDeclaredConstructors()
:獲取所有構(gòu)造方法。
通常我們調(diào)用類的構(gòu)造方法,這樣寫的(以 StringBuilder
為例):
// 實(shí)例化StringBuilder對(duì)象
StringBuilder name = new StringBuilder("Hello Imooc");
通過反射,要先獲取 Constructor
對(duì)象,再調(diào)用 Class.newInstance()
方法:
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectionDemo {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {
// 獲取構(gòu)造方法
Constructor constructor = StringBuffer.class.getConstructor(String.class);
// 調(diào)用構(gòu)造方法
Object str = constructor.newInstance("Hello Imooc");
System.out.println(str);
}
}
運(yùn)行結(jié)果:
Hello Imooc
5. 訪問字段
前面我們知道了如何獲取 Class
實(shí)例,只要獲取了 Class
實(shí)例,就可以獲取它的所有信息。
5.1 獲取字段
Field 類代表某個(gè)類中的一個(gè)成員變量,并提供動(dòng)態(tài)的訪問權(quán)限。Class
提供了以下幾個(gè)方法來獲取字段:
Field getField(name)
:根據(jù)屬性名獲取某個(gè)public
的字段(包含父類繼承);Field getDeclaredField(name)
:根據(jù)屬性名獲取當(dāng)前類的某個(gè)字段(不包含父類繼承);Field[] getFields()
:獲得所有的public
字段(包含父類繼承);Field[] getDeclaredFields()
:獲取當(dāng)前類的所有字段(不包含父類繼承)。
獲取字段的實(shí)例如下:
package com.imooc.reflect;
import java.lang.reflect.Field;
public class ImoocStudent1 {
// 昵稱 私有字段
private String nickname;
// 余額 私有字段
private float balance;
// 職位 公有字段
public String position;
public static void main(String[] args) throws NoSuchFieldException {
// 類名.class 方式獲取 Class 實(shí)例
Class cls1 = ImoocStudent1.class;
// 獲取 public 的字段 position
Field position = cls1.getField("position");
System.out.println(position);
// 獲取字段 balance
Field balance = cls1.getDeclaredField("balance");
System.out.println(balance);
// 獲取所有字段
Field[] declaredFields = cls1.getDeclaredFields();
for (Field field: declaredFields) {
System.out.print("name=" + field.getName());
System.out.println("\ttype=" + field.getType());
}
}
}
運(yùn)行結(jié)果:
public java.lang.String com.imooc.reflect.ImoocStudent1.position
private float com.imooc.reflect.ImoocStudent1.balance
name=nickname type=class java.lang.String
name=balance type=float
name=position type=class java.lang.String
ImoocStudent1
類中含有 3 個(gè)屬性,其中 position
為公有屬性,nickname
和 balance
為私有屬性。我們通過類名.class
的方式獲取了 Class
實(shí)例,通過調(diào)用其實(shí)例方法并打印其返回結(jié)果,驗(yàn)證了獲取字段,獲取單個(gè)字段方法,在沒有找到該指定字段的情況下,會(huì)拋出一個(gè) NoSuchFieldException
。
調(diào)用獲取所有字段方法,返回的是一個(gè) Field
類型的數(shù)組??梢哉{(diào)用 Field
類下的 getName()
方法來獲取字段名稱,getType()
方法來獲取字段類型。
5.2 獲取字段值
既然我們已經(jīng)獲取到了字段,那么就理所當(dāng)然地可以獲取字段的值。可以通過 Field
類下的 Object get(Object obj)
方法來獲取指定字段的值,方法的參數(shù) Object
為對(duì)象實(shí)例,實(shí)例如下:
package com.imooc.reflect;
import java.lang.reflect.Field;
public class ImoocStudent2 {
public ImoocStudent2() {
}
public ImoocStudent2(String nickname, String position) {
this.nickname = nickname;
this.position = position;
}
// 昵稱 私有字段
private String nickname;
// 職位 公有屬性
public String position;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 實(shí)例化一個(gè) ImoocStudent2 對(duì)象
ImoocStudent2 imoocStudent2 = new ImoocStudent2("小慕", "架構(gòu)師");
Class cls = imoocStudent2.getClass();
Field position = cls.getField("position");
Object o = position.get(imoocStudent2);
System.out.println(o);
}
}
運(yùn)行結(jié)果:
架構(gòu)師
ImoocStudent2
內(nèi)部分別包含一個(gè)公有屬性 position
和一個(gè)私有屬性 nickname
,我們首先實(shí)例化了一個(gè) ImoocStudent2 對(duì)象,并且獲取了與其對(duì)應(yīng)的 Class
對(duì)象,然后調(diào)用 getField()
方法獲取了 position
字段,通過調(diào)用 Field
類下的實(shí)例方法 Object get(Object obj)
來獲取了 position
字段的值。
這里值得注意的是,如果我們想要獲取 nickname
字段的值會(huì)稍有不同,因?yàn)樗撬接袑傩?,我們看?get()
方法會(huì)拋出 IllegalAccessException
異常,如果直接調(diào)用 get()
方法獲取私有屬性,就會(huì)拋出此異常。
想要獲取私有屬性,必須調(diào)用 Field.setAccessible(boolean flag)
方法來設(shè)置該字段的訪問權(quán)限為 true
,表示可以訪問。在 main()
方法中,獲取私有屬性 nickname
的值的實(shí)例如下:
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 實(shí)例化一個(gè) ImoocStudent2 對(duì)象
ImoocStudent2 imoocStudent2 = new ImoocStudent2("小慕", "架構(gòu)師");
Class cls = imoocStudent2.getClass();
Field nickname = cls.getDeclaredField("nickname");
// 設(shè)置可以訪問
nickname.setAccessible(true);
Object o = nickname.get(imoocStudent2);
System.out.println(o);
}
此時(shí),就不會(huì)拋出異常,運(yùn)行結(jié)果:
小慕
5.2 為字段賦值
為字段賦值也很簡(jiǎn)單,調(diào)用 Field.set(Object obj, Object value)
方法即可,第一個(gè) Object
參數(shù)是指定的實(shí)例,第二個(gè) Object
參數(shù)是待修改的值。我們直接來看實(shí)例:
package com.imooc.reflect;
import java.lang.reflect.Field;
public class ImoocStudent3 {
public ImoocStudent3() {
}
public ImoocStudent3(String nickname) {
this.nickname = nickname;
}
// 昵稱 私有字段
private String nickname;
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 實(shí)例化一個(gè) ImoocStudent3 對(duì)象
ImoocStudent3 imoocStudent3 = new ImoocStudent3("小慕");
Class cls = imoocStudent3.getClass();
Field nickname = cls.getDeclaredField("nickname");
nickname.setAccessible(true);
// 設(shè)置字段值
nickname.set(imoocStudent3, "Colorful");
// 打印設(shè)置后的內(nèi)容
System.out.println(imoocStudent3.getNickname());
}
}
運(yùn)行結(jié)果:
Colorful
6. 調(diào)用方法
Method 類代表某一個(gè)類中的一個(gè)成員方法。
6.1 獲取方法
Class
提供了以下幾個(gè)方法來獲取方法:
Method getMethod(name, Class...)
:獲取某個(gè)public
的方法(包含父類繼承);Method getgetDeclaredMethod(name, Class...)
:獲取當(dāng)前類的某個(gè)方法(不包含父類);Method[] getMethods()
:獲取所有public
的方法(包含父類繼承);Method[] getDeclareMethods()
:獲取當(dāng)前類的所有方法(不包含父類繼承)。
獲取方法和獲取字段大同小異,只需調(diào)用以上 API
即可,這里不再贅述。
6.2 調(diào)用方法
獲取方法的目的就是調(diào)用方法,調(diào)用方法也就是讓方法執(zhí)行。
通常情況下,我們是這樣調(diào)用對(duì)象下的實(shí)例方法(以 String
類的 replace()
方法為例):
String name = new String("Colorful");
String result = name.replace("ful", "");
改寫成通過反射方法調(diào)用:
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ReflectionDemo1 {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 實(shí)例化字符串對(duì)象
String name = new String("Colorful");
// 獲取 method 對(duì)象
Method method = String.class.getMethod("replace", CharSequence.class, CharSequence.class);
// 調(diào)用 invoke() 執(zhí)行方法
String result = (String) method.invoke(name, "ful", "");
System.out.println(result);
}
}
運(yùn)行結(jié)果:
Color
代碼中,調(diào)用 Method
實(shí)例的 invoke(Object obj, Object...args)
方法,就是通過反射來調(diào)用了該方法。
其中 invoke()
方法的第一個(gè)參數(shù)為對(duì)象實(shí)例,緊接著的可變參數(shù)就是要調(diào)用方法的參數(shù),參數(shù)要保持一致。
7. 反射應(yīng)用
Tips: 理解此部分內(nèi)容可能需要閱讀者有一定的開發(fā)經(jīng)驗(yàn)
學(xué)習(xí)完了反射,大家可能依然非常疑惑,反射似乎離我們的實(shí)際開發(fā)非常遙遠(yuǎn),實(shí)際情況也的確是這樣的。因?yàn)槲覀冊(cè)趯?shí)際開發(fā)中基本不會(huì)用到反射。下面我來分享一個(gè)實(shí)際開發(fā)中應(yīng)用反射的案例。
場(chǎng)景是這樣的:有一個(gè)文件上傳系統(tǒng),文件上傳系統(tǒng)有多種不同的方式(上傳到服務(wù)器本地、上傳到七牛云、阿里云 OSS 等),因此就有多個(gè)不同的文件上傳實(shí)現(xiàn)類。系統(tǒng)希望通過配置文件來獲取用戶的配置,再去實(shí)例化對(duì)應(yīng)的實(shí)現(xiàn)類。因此,我們一開始的思路可能是這樣的(偽代碼):
public class UploaderFactory {
// 通過配置文件獲取到的配置,可能為 local(上傳到本地) qiniuyun(上傳到七牛)
private String uploader;
// 創(chuàng)建實(shí)現(xiàn)類對(duì)象的方法
public Uploader createUploader() {
switch (uploader) {
case "local":
// 實(shí)例化上傳到本地的實(shí)現(xiàn)類
return new LocalUploader();
case "qiniuyun":
// 實(shí)例化上傳到七牛云的實(shí)現(xiàn)類
return new QiniuUploader();
default:
break;
}
return null;
}
}
createUploader()
就是創(chuàng)建實(shí)現(xiàn)類的方法,它通過 switch case
結(jié)構(gòu)來判斷從配置文件中獲取的 uploader
變量。
這看上去似乎沒有什么問題,但試想,后續(xù)我們的實(shí)現(xiàn)類越來越多,就需要一直向下添加 case
語(yǔ)句,并且要約定配置文件中的字符串要和 case
匹配才行。這樣的代碼既不穩(wěn)定也不健壯。
換一種思路考慮問題,我們可以通過反射機(jī)制來改寫這里的代碼。首先,約定配置文件的 uploader
配置項(xiàng)不再是字符串,改為類的全路徑命名。因此,在 createUploader()
方法中不再需要 switch case
結(jié)構(gòu)來判斷,直接通過 Class.forName(uploader)
就可以獲取 Class
實(shí)例,并調(diào)用其構(gòu)造方法實(shí)例化對(duì)應(yīng)的文件上傳對(duì)象,偽代碼如下:
public class UploaderFactory {
// 通過配置文件獲取到的配置,實(shí)現(xiàn)類的包名.類名
private String uploader;
// 創(chuàng)建實(shí)現(xiàn)類對(duì)象的方法
public Uploader createUploader() {
// 獲取構(gòu)造方法
Constructor constructor = Class.forName(uploader).getConstructor();
return (Uploader) constructor.newInstance();
}
}
通過反射實(shí)例化對(duì)應(yīng)的實(shí)現(xiàn)類,我們不需要再維護(hù) UploaderFactory
下的代碼,其實(shí)現(xiàn)類的命名、放置位置也不受約束,只需要在配置文件中指定類名全路徑即可。
8. 小結(jié)
通過本小節(jié)的學(xué)習(xí),我們知道了反射是 Java 提供的一種機(jī)制,它可以在程序的運(yùn)行狀態(tài)中,動(dòng)態(tài)獲取類的信息,注入類內(nèi)部的屬性和方法,完成對(duì)象的實(shí)例化等操作。獲取 Class
對(duì)象有 3 種方法,通過學(xué)習(xí)反射的相關(guān)接口,我們了解到通過反射可以實(shí)現(xiàn)一切我們想要的操作。在本小節(jié)的最后,我也分享了一個(gè)我在實(shí)際開發(fā)中應(yīng)用反射的案例,希望能對(duì)大家有所啟發(fā)。