一、反射機制
1.1 框架
在學習Java的路上,相信你一定使用過各種各樣的框架。所謂的框架就是一個半成品軟件,已經對基礎的代碼進行了封裝並提供相應的API。在框架的基礎上進行軟件開發,可以簡化編碼。學習使用框架並不需要了解反射,但是如果想要自己寫一個框架,那麼就需要對反射機制有很深入的瞭解。
1.2 什麼是反射機制?
在程序運行狀態中,對於任意一個類或對象,都能夠獲取到這個類的所有屬性和方法(包括私有屬性和方法),這種動態獲取信息以及動態調用對象方法的功能就稱爲反射機制。簡單來講,通過反射,類對我們是完全透明的,想要獲取任何東西都可以。
1.3 反射的優點
- 可以在程序運行過程中,操作這些對象;
- 可以解耦,提高程序的可擴展性。
在學習反射以前,我們先來了解一下Java代碼在計算機中所經歷的三個階段:
- Source源代碼階段:.java被編譯成*.class字節碼文件。
- Class類對象階段:.class字節碼文件被類加載器加載進內存,並將其封裝成Class對象(用於在內存中描述字節碼文件),Class對象將原字節碼文件中的成員變量抽取出來封裝成數組Field[],將原字節碼文件中的構造函數抽取出來封裝成數組Construction[],將成員方法封裝成數組Method[]。當然Class類內不止這三個,還封裝了很多,我們常用的就這三個。
- RunTime運行時階段:使用new創建對象的過程。
二、獲取Class對象
2.1 獲取Class對象的三種方式
- 【Source源代碼階段】 Class.forName(“全類名”):將字節碼文件加載進內存,返回Class對象;
多用於配置文件,將類名定義在配置文件中,通過讀取配置文件加載類。 - 【Class類對象階段】 類名.class:通過類名的屬性class獲取;
多用於參數的傳遞 - 【Runtime運行時階段】對象.getClass():此方法是定義在Objec類中的方法,因此所有的類都會繼承此方法。
多用於對象獲取字節碼的方式
2.2 方法演示
public class getClass {
public static void main(String[] args) throws Exception {
//方式一:Class.forName("全類名");
Class class1 = Class.forName("zzuli.edu.cn.Person"); //Person自定義實體類
System.out.println("class1 = " + class1);
//方式二:類名.class
Class class2 = Person.class;
System.out.println("class2 = " + class2);
//方式三:對象.getClass();
Person person = new Person();
Class class3 = person.getClass();
System.out.println("class3 = " + class3);
//比較三個對象
System.out.println(class1 == class2); //true
System.out.println(class1 == class3); //true
}
}
運行結果:
通過上述比較三個對象的結果可以得出一個結論:同一個字節碼文件(*.class)在一次程序運行過程中,只會被加載一次,無論通過哪一種方式獲取的Class對象都是同一個。
三、 Class對象的功能
3.1 獲取功能
這裏只寫出一些常用的,具體可以參看jdk的幫助文檔。
- 獲取成員變量
Field[] getFields() //獲取所有public修飾的成員變量
Field getField(String name) //獲取指定名稱的public修飾的成員變量
Field[] getDeclaredFields() //獲取所有的成員變量,不考慮修飾符
Field getDeclaredField(String name) //獲取指定的成員變量,不考慮修飾符
- 獲取構造方法
Constructor<?>[] getConstructors() //獲取所有public修飾的構造函數
Constructor<T> getConstructor(類<?>... parameterTypes) //獲取指定的public修飾的構造函數
Constructor<?>[] getDeclaredConstructors() //獲取所有的構造函數,不考慮修飾符
Constructor<T> getDeclaredConstructor(類<?>... parameterTypes) //獲取指定的構造函數,不考慮修飾符
- 獲取成員方法
Method[] getMethods() //獲取所有public修飾的成員方法
Method getMethod(String name, 類<?>... parameterTypes) //獲取指定名稱的public修飾的成員方法
Method[] getDeclaredMethods() //獲取所有的成員方法,不考慮修飾符
Method getDeclaredMethod(String name, 類<?>... parameterTypes) //獲取指定名稱的成員方法,不考慮修飾符
- 獲取全類名
String getName()
3.2 Field:成員變量
- (1)設置值 void set(Object obj, Object value)
- (2)獲取值 get(Object obj)
- (3)忽略訪問權限修飾符的安全檢查 setAccessible(true):暴力反射
3.2.1 測試的實體類
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
public class Person {
public String a; //最大範圍public
protected String b; //受保護類型
String c; //默認的訪問權限
private String d; //私有類型
}
3.2.2 測試getFields和getField(String name)方法
/**
* 獲取成員變量
* * Field[] getFields()
* * Field getField(String name)
* @throws Exception
*/
public class reflectDemo1{
public static void main(String[] args) throws Exception {
//獲取Person的Class對象
Class personClass = Person.class;
//1、Field[] getFields()獲取所有public修飾的成員變量
Field[] fields = personClass.getFields();
for(Field field : fields){
System.out.println(field);
}
System.out.println("=============================");
//2.Field getField(String name) 獲取指定名稱的public修飾的成員變量
Field a = personClass.getField("a");
//獲取成員變量a的值 [也只能獲取公有的,獲取私有的或者不存在的字符會拋出異常]
Person p = new Person();
Object value = a.get(p);
System.out.println("value = " + value);//因爲在Person類中a沒有賦值,所以爲null
//設置成員變量a的屬性值
a.set(p,"張三");
System.out.println(p);
}
}
運行結果:
3.2.3 測試 getDeclaredFields 和 getDeclaredField(String name)方法
/**
* Field[] getDeclaredFields()
* Field getDeclaredField(String name)
* @throws Exception
*/
public class reflectDemo2 {
public static void main(String[] args) throws Exception {
//獲取Person的Class對象
Class personClass = Person.class;
//Field[] getDeclaredFields():獲取所有的成員變量,不考慮修飾符
Field[] declaredFields = personClass.getDeclaredFields();
for(Field filed : declaredFields){
System.out.println(filed);
}
System.out.println("===================================");
//Field getDeclaredField(String name) //獲取指定的成員變量,不考慮修飾符
Field d = personClass.getDeclaredField("d"); //private String d;
Person p = new Person();
//Object value1 = d.get(p); //如果直接獲取會拋出異常,因爲對於私有變量雖然能會獲取到,但不能直接set和get,必須忽略訪問權限修飾符的安全檢查後纔可以
//System.out.println("value1 = " + value1);
//忽略訪問權限修飾符的安全檢查,又稱爲暴力反射
d.setAccessible(true);
Object value2 = d.get(p);
System.out.println("value2 = " + value2);
}
}
運行結果
注意:如果沒有忽略訪問修飾符直接訪問會拋出如下所示的異常
3.3 Constructor:構造方法
創建對象:T newInstance(Object… initargs)
注意:如果使用空參數構造方法創建對象,操作可以簡化:直接使用Class對象的newInstance方法。
3.3.1 修改測試的實體類
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
public class Person {
private String name;
private Integer age;
//無參構造函數
public Person() {
}
//單個參數的構造函數,且爲私有構造方法
private Person(String name){
}
//有參構造函數
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
3.3.2 測試獲取構造函數的方法
/**
* 獲取構造方法
* Constructor<?>[] getConstructors()
* Constructor<T> getConstructor(類<?>... parameterTypes)
*/
public class reflectDemo3 {
public static void main(String[] args) throws Exception {
//獲取Person的Class對象
Class personClass = Person.class;
//Constructor<?>[] getConstructors() //獲取所有public修飾的構造函數
Constructor[] constructors = personClass.getConstructors();
for(Constructor constructor : constructors){
System.out.println(constructor);
}
System.out.println("==========================================");
//獲取無參構造函數 注意:Person類中必須要有無參的構造函數,不然拋出異常
Constructor constructor1 = personClass.getConstructor();
System.out.println("constructor1 = " + constructor1);
//使用獲取到的無參構造函數創建對象
Object person1 = constructor1.newInstance();
System.out.println("person1 = " + person1);
System.out.println("==========================================");
//獲取有參的構造函數 //public Person(String name, Integer age) 參數類型順序要與構造函數內一致,且參數類型爲字節碼文件類型
Constructor constructor2 = personClass.getConstructor(String.class,Integer.class);
System.out.println("constructor2 = " + constructor2);
//使用獲取到的有參構造函數創建對象
Object person2 = constructor2.newInstance("zhangsan", 22); //獲取的是有參的構造方法,就必須要指定參數
System.out.println(person2);
System.out.println("=========================================");
//對於一般的無參構造函數,我們都不會先獲取無參構造器之後在進行初始化,而是直接調用Class類內的newInstance()方法
Object person3 = personClass.newInstance();
System.out.println("person3 = " + person3);
}
}
運行結果:
總結:如果使用無參構造方法創建對象,操作可以進行簡化:直接使用Class對象的newInstance方法創建即可。
對於多出個Declared關鍵詞的兩個方法,與不帶這個詞的兩個方法的對比,與之前3.2敘述的一樣,getDeclaredConstructor方法可以獲取到任何訪問權限的構造器,而getConstructor方法只能獲取public修飾的構造器,具體不再測試。此外在構造器的對象內也有setAccessible(true)方法,把它設置成true就可以操作了。
3.4 Method:方法對象
- 執行方法:Object invoke(Object obj, Object… args)
3.4.1 修改測試的實體類
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Setter
@Getter
@ToString
public class Person {
private String name;
private Integer age;
//無參構造函數
public Person() {
}
//有參構造函數
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
//無參方法
public void eat(){
System.out.println("eat...");
}
//重載有參方法
public void eat(String food){
System.out.println("eat..."+food);
}
}
3.4.2 測試獲取成員方法的方法
/**
* 獲取成員方法
* * Method[] getMethods()
* * Method getMethod(String name, 類<?>... parameterTypes)
*/
public class reflectDemo4 {
public static void main(String[] args) throws Exception {
//獲取Person的Class對象
Class personClass = Person.class;
//獲取指定名稱的方法
Method eat_method1 = personClass.getMethod("eat");
//執行方法
Person person = new Person();
Object rtValue = eat_method1.invoke(person);//如果方法有返回值類型可以獲取到,沒有就爲null
//因爲eat方法沒有返回值,故輸出null
System.out.println("rtValue = " + rtValue);
System.out.println("--------------------------------------------");
//獲取有參的函數,有兩個參數:第一個參數爲方法名,第二個參數是獲取方法的參數類型的字節碼文件
Method eat_method2 = personClass.getMethod("eat", String.class);
//執行方法
eat_method2.invoke(person,"蘋果");
System.out.println("============================================");
//獲取方法列表
Method[] methods = personClass.getMethods();
for(Method method : methods){ //注意:獲取到的方法不僅僅是Person類內自己的方法
System.out.println(method); //繼承Object中的方法也會被獲取到(當然前提是public修飾的)
}
}
}
運行結果
我們可以看出Person內的public方法都被打印出來了,此外Object中的public方法也都被打印出來了。
同之前的敘述一樣,帶有Declared關鍵字的方法這兩個方法,可以獲取到任意修飾符的方法。同樣也提供了setAccessible(true)方法進行暴力反射。
綜上所述:在反射面前沒有公有私有,都可以通過暴力反射解決。
3.5 獲取全類名
getName()方法獲取的類名是全類名(帶有路徑)
public class getNameDemo {
public static void main(String[] args) throws Exception {
//獲取Person的Class對象
Class personClass = Person.class;
//獲取全類名
String className = personClass.getName();
System.out.println(className);
}
}
運行結果
四、反射機制的應用案例
4.1 案例分析
4.1.1 需求
寫一個"框架",在不改變該類的任何代碼的前提下,可以幫我們創建任意類的對象,並且執行其中的任意方法。
4.1.2 實現
(1)配置文件
(2)反射機制
4.1.3 步驟
(1)將需要創建的對象的全類名和需要執行的方法定義在配置文件中
(2)在程序中加載讀取配置文件
(3)使用反射技術把類文件加載進內存
(4)創建對象
(5)執行方法
4.2 代碼實現
4.2.1 需要的實體類
(1)Person類
public class Person {
//無參方法
public void eat(){
System.out.println("eat...");
}
}
(2)Student類
public class Student {
//無參方法
public void study(){
System.out.println("I am a Student");
}
}
4.2.2 編寫配置文件
className = zzuli.edu.cn.Person
methodName = eat
4.2.3 實現框架
/**
* 前提:不能改變該類的任何代碼。可以創建任意類的對象,可以執行任意方法
* 即:拒絕硬編碼
*/
public class ReflectTest {
public static void main(String[] args) throws Exception {
//1.加載配置文件
//1.1創建Properties對象
Properties pro = new Properties();
//1.2加載配置文件
//1.2.1獲取class目錄下的配置文件(使用類加載器)
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("pro.properties");
pro.load(inputStream);
//2.獲取配置文件中定義的數據
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");
//3.加載該類進內存
Class cls = Class.forName(className);
//4.創建對象
Object obj = cls.newInstance();
//5.獲取方法對象
Method method = cls.getMethod(methodName);
//6.執行方法
method.invoke(obj);
}
}
4.2.4 運行結果
4.2.5 修改配置文件,再次運行
//將配置文件內的信息修改爲Student類及類內的study方法
className = zzuli.edu.cn.Student
methodName = study
運行結果
優點:對於框架來說,已經對基礎的代碼進行了封裝並提供相應的API。在框架的基礎上進行軟件開發,可以簡化編碼。但如果我們使用傳統的new形式來實例化,那麼當類更改時我們就要修改Java代碼,這是很繁瑣的。修改Java代碼以後我們還要進行重新編譯、測試、發佈等一系列的操作。而如果我們僅僅只是修改配置文件,而不需要修改Java代碼就簡單的多。此外使用反射還能達到解耦的效果,如果我們使用的是new這種形式進行對象的實例化。此時如果在項目的某一個小模塊中我們的一個實例類丟失了,那麼在編譯期間就會報錯,會導致整個項目無法啓動。而對於反射創建對象Class.forName(“全類名”)這種形式,我們在編譯期需要的僅僅只是一個字符串(全類名),在編譯期不會報錯,這樣其他的模塊就可以正常的運行,而不會因爲一個模塊的問題導致整個項目崩潰。
如果你認真看完了這篇文章,反射就應該掌握的差不多了,在接下來的幾篇文章中,我會使用反射來實現熱加載、反編譯,並且使用反射來簡單封裝一個Spring框架和MyBatis框架。如果想要對反射機制有更深入的瞭解,可以看後續的文章。