程序員,你連反射都不會,還敢說自己會Java嗎?

一、反射機制

1.1 框架

在學習Java的路上,相信你一定使用過各種各樣的框架。所謂的框架就是一個半成品軟件,已經對基礎的代碼進行了封裝並提供相應的API。在框架的基礎上進行軟件開發,可以簡化編碼。學習 使用框架並不需要了解反射,但是如果想要自己寫一個框架,那麼就需要對反射機制有很深入的瞭解。

1.2 什麼是反射機制?

在程序運行狀態中,對於任意一個類或對象,都能夠獲取到這個類的所有屬性和方法(包括私有屬性和方法),這種動態獲取信息以及動態調用對象方法的功能就稱為反射機制。簡單來講,通過反射,類對我們是完全透明的,想要獲取任何東西都可以。

1.3 反射的優點

  1. 可以在程序運行過程中,操作這些對象;
  2. 可以解耦,提高程序的可擴展性。

在學習反射以前,我們先來了解一下Java代碼在計算機中所經歷的三個階段:

  1. Source源代碼階段:.java被編譯成*.class字節碼文件。
  2. Class類對象階段:.class字節碼文件被類加載器加載進內存,並將其封裝成Class對象(用於在內存中描述字節碼文件),Class對象將原字節碼文件中的成員變量抽取出來封裝成數組Field[],將原字節碼文件中的構造函數抽取出來封裝成數組Construction[],將成員方法封裝成數組Method[]。當然Class類內不止這三個,還封裝了很多,我們常用的就這三個。
  3. RunTime運行時階段:使用new創建對象的過程。
程序員,你連反射都不會,還敢說自己會Java嗎?


二、獲取Class對象

2.1 獲取Class對象的三種方式

  1. 【Source源代碼階段】 Class.forName(“全類名”):將字節碼文件加載進內存,返回Class對象;
    多用於配置文件,將類名定義在配置文件中,通過讀取配置文件加載類。
  2. 【Class類對象階段】 類名.class:通過類名的屬性class獲取;
    多用於參數的傳遞
  3. 【Runtime運行時階段】對象.getClass():此方法是定義在Objec類中的方法,因此所有的類都會繼承此方法。
    多用於對象獲取字節碼的方式

2.2 方法演示

<code>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
    }
}/<code>

運行結果:

程序員,你連反射都不會,還敢說自己會Java嗎?


程序員,你連反射都不會,還敢說自己會Java嗎?


通過上述比較三個對象的結果可以得出一個結論:同一個字節碼文件(.class)在一次程序運行過程中,只會被加載一次,無論通過哪一種方式獲取的Class對象都是同一個。*

三、 Class對象的功能

3.1 獲取功能

這裡只寫出一些常用的,具體可以參看jdk的幫助文檔。

  1. 獲取成員變量
<code>Field[] getFields()          //獲取所有public修飾的成員變量
Field getField(String name)  //獲取指定名稱的public修飾的成員變量

Field[] getDeclaredFields()  //獲取所有的成員變量,不考慮修飾符
Field getDeclaredField(String name)  //獲取指定的成員變量,不考慮修飾符/<code>
  1. 獲取構造方法
<code>Constructor>[] getConstructors() //獲取所有public修飾的構造函數
Constructor getConstructor(類>... parameterTypes)  //獲取指定的public修飾的構造函數

Constructor>[] getDeclaredConstructors()  //獲取所有的構造函數,不考慮修飾符
Constructor getDeclaredConstructor(類>... parameterTypes)  //獲取指定的構造函數,不考慮修飾符/<code>
  1. 獲取成員方法
<code>Method[] getMethods()           //獲取所有public修飾的成員方法
Method getMethod(String name, 類>... parameterTypes) //獲取指定名稱的public修飾的成員方法

Method[] getDeclaredMethods()  //獲取所有的成員方法,不考慮修飾符
Method getDeclaredMethod(String name, 類>... parameterTypes) //獲取指定名稱的成員方法,不考慮修飾符/<code>
  1. 獲取全類名
<code>String getName()/<code>

3.2 Field:成員變量

  • (1)設置值 void set(Object obj, Object value)
  • (2)獲取值 get(Object obj)
  • (3)忽略訪問權限修飾符的安全檢查 setAccessible(true):暴力反射

3.2.1 測試的實體類

<code>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;       //私有類型

}/<code>

3.2.2 測試getFields和getField(String name)方法

<code>/**
 *      獲取成員變量
 *    * 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);
    }
}/<code>

運行結果:


程序員,你連反射都不會,還敢說自己會Java嗎?


3.2.3 測試 getDeclaredFields 和 getDeclaredField(String name)方法

<code>/**
 *     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);
    }
}/<code>

運行結果


程序員,你連反射都不會,還敢說自己會Java嗎?


注意:如果沒有忽略訪問修飾符直接訪問會拋出如下所示的異常


程序員,你連反射都不會,還敢說自己會Java嗎?


3.3 Constructor:構造方法

創建對象:T newInstance(Object… initargs)注意:如果使用空參數構造方法創建對象,操作可以簡化:直接使用Class對象的newInstance方法。

3.3.1 修改測試的實體類

<code>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;
    }
}/<code>

3.3.2 測試獲取構造函數的方法

<code>/**
 *    獲取構造方法
 *    Constructor>[] getConstructors()
 *    Constructor 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);
    }
}/<code>

運行結果:


程序員,你連反射都不會,還敢說自己會Java嗎?


總結:如果使用無參構造方法創建對象,操作可以進行簡化:直接使用Class對象的newInstance方法創建即可。

對於多出個Declared關鍵詞的兩個方法,與不帶這個詞的兩個方法的對比,與之前3.2敘述的一樣,getDeclaredConstructor方法可以獲取到任何訪問權限的構造器,而getConstructor方法只能獲取public修飾的構造器,具體不再測試。此外在構造器的對象內也有setAccessible(true)方法,把它設置成true就可以操作了。

3.4 Method:方法對象

  • 執行方法:Object invoke(Object obj, Object… args)

3.4.1 修改測試的實體類

<code>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);
    }
}/<code>

3.4.2 測試獲取成員方法的方法

<code>/**
 *     獲取成員方法
 *    * 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修飾的)
        }
    }
}/<code> 

運行結果


程序員,你連反射都不會,還敢說自己會Java嗎?


我們可以看出Person內的public方法都被打印出來了,此外Object中的public方法也都被打印出來了。

同之前的敘述一樣,帶有Declared關鍵字的方法這兩個方法,可以獲取到任意修飾符的方法。同樣也提供了setAccessible(true)方法進行暴力反射。

綜上所述:在反射面前沒有公有私有,都可以通過暴力反射解決。

3.5 獲取全類名

getName()方法獲取的類名是全類名(帶有路徑)

<code>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);
    }
}/<code>

運行結果


程序員,你連反射都不會,還敢說自己會Java嗎?


四、反射機制的應用案例

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類

<code>public class Person {
    //無參方法
    public void eat(){
        System.out.println("eat...");
    }
}/<code>

(2)Student類

<code>public class Student {
    //無參方法
    public void study(){
        System.out.println("I am a Student");
    }
}/<code>

4.2.2 編寫配置文件

<code>className = zzuli.edu.cn.Person
methodName = eat/<code>

4.2.3 實現框架

<code>/**
  * 前提:不能改變該類的任何代碼。可以創建任意類的對象,可以執行任意方法
  * 即:拒絕硬編碼
  */
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);
    }
}/<code>

4.2.4 運行結果


程序員,你連反射都不會,還敢說自己會Java嗎?


4.2.5 修改配置文件,再次運行

<code>//將配置文件內的信息修改為Student類及類內的study方法
className = zzuli.edu.cn.Student
methodName = study/<code>

運行結果


程序員,你連反射都不會,還敢說自己會Java嗎?

程序員,你連反射都不會,還敢說自己會Java嗎?


優點:對於框架來說,已經對基礎的代碼進行了封裝並提供相應的API。在框架的基礎上進行軟件開發,可以簡化編碼。但如果我們使用傳統的new形式來實例化,那麼當類更改時我們就要修改Java代碼,這是很繁瑣的。修改Java代碼以後我們還要進行重新編譯、測試、發佈等一系列的操作。而如果我們僅僅只是修改配置文件,而不需要修改Java代碼就簡單的多。此外使用反射還能達到解耦的效果,如果我們使用的是new這種形式進行對象的實例化。此時如果在項目的某一個小模塊中我們的一個實例類丟失了,那麼在編譯期間就會報錯,會導致整個項目無法啟動。而對於反射創建對象Class.forName(“全類名”)這種形式,我們在編譯期需要的僅僅只是一個字符串(全類名),在編譯器不會報錯,這樣其他的模塊就可以正常的運行,而不會因為一個模塊的問題導致整個項目崩潰。

最後,如果你認真看完了這篇文章,反射就應該掌握的差不多了,關注我,後續會有更多內容更新發布!


分享到:


相關文章: