Java中的反射機制詳解

一、什麼是反射?

(1)Java反射機制的核心是在程序運行時動態加載類並獲取類的詳細信息,從而操作類或對象的屬性和方法。本質是JVM得到class對象之後,再通過class對象進行反編譯,從而獲取對象的各種信息。

(2)Java屬於先編譯再運行的語言,程序中對象的類型在編譯期就確定下來瞭,而當程序在運行時可能需要動態加載某些類,這些類因為之前用不到,所以沒有被加載到JVM。通過反射,可以在運行時動態地創建對象並調用其屬性,不需要提前在編譯期知道運行的對象是誰。

二、為什麼要用反射

首先,我們擁有一個接口 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 語言的反射機制。

三、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

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

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

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

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

四、獲取 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 做個對比。

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

上面我們介紹瞭獲取 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實現。詳細源碼此處就不深究瞭,瞭解即可。

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

和獲取構造函數差不多,獲取成員變量也分批量獲取和單個獲取。返回值通過 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 屬性賦值
    }
}
 

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

同樣的,獲取成員方法也分批量獲取和單個獲取。返回值通過 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);
    }
}
 

八. 反射機制優缺點

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

缺點

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

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

九. 反射的經典應用場景

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

動態代理機制使用 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);
}

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容

推薦閱讀: