Java高級特性-反射:不寫死在代碼,還怎麼 new 對象?

反射是 Java 的一個高級特性,大量用在各類開源框架上。程序員

在開源框架中,每每以同一套算法,來應對不一樣的數據結構。好比,Spring 的依賴注入,咱們不用本身 new 對象了,這工做交給 Spring 去作。算法

然而,咱們要 new 一個對象,就得寫在代碼上。但 Spring 確定猜不到咱們的類叫什麼名字,那 Spring 又是怎麼把對象給 new 出來的呢?數據庫

這就離不開反射。數據結構

反射的意義與做用

Java 有兩種操做類的方式,分別是:非反射、反射。框架

先來講第一種方式,非反射。this

非反射,就是根據代碼,靜態地操做類。好比,下面這段代碼:code

public class Application {
    public static void main(String[] args) {
        // 建立一個用戶對象
        ClientUser client = new ClientUser();
    }
}

這個 main() 方法很簡單,就是建立一個用戶對象。整個過程是這樣的,在 JVM 運行前,你必須先想好要建立哪些對象,而後寫在代碼上,最後你運行 main() 方法,JVM 給你建立一個用戶對象。orm

簡單來講,你寫好代碼,扔給 JVM 運行,運行完就沒了。對象

在這種狀況下,程序員必須控制一切,建立什麼對象得提早寫死在代碼上。好比,我要多建立一個商戶對象,那就得改代碼:get

public class Application {

    public static void main(String[] args) {
        // 建立一個用戶對象
        ClientUser client = new ClientUser();
        // 建立一個商戶對象
        ShopUser shop = new ShopUser();
        // 省略無數 new 操做
    }
}

若是按照這種作法,只要需求一變,程序員就得改代碼,工做效率很低。好比說,你碰上覆雜些的項目,不光得建立對象,還得 set 成員變量。這樣一來,每新加一個對象,你就得改一堆代碼,早晚得累死。

那這些工做能簡化嗎?

這要用到第二種操做類的方式,反射。反射是一種動態操做類的機制。好比,我要建立一堆對象,那不用提早寫死在代碼,而是放在配置文件或者數據庫上,等到程序運行的時候,再讀取配置文件建立對象。

仍是上面的代碼,通過反射的改造,就變成這個樣子:

public class Application {

    // 模擬配置文件
    private static Set<String> configs = new HashSet<>();
    
    static {
        configs.add("com.jiarupc.reflection.ShopUser");
        configs.add("com.jiarupc.reflection.ClientUser");
        // 省略無數配置
    }
    
    public static void main(String[] args) throws Exception {
        // 讀取配置文件
        for (String config : configs) {
            // 經過配置文件,獲取類的Class對象
            Class clazz = Class.forName(config);
            // 建立對象
            Object object = clazz.newInstance();
            System.out.println(object);
        }
    }
}

當你運行 main() 方法的時候,程序會先讀取配置文件,而後根據配置文件建立對象。用了反射後,你有沒有發現,工做變輕鬆了。一旦新加對象,咱們只要加一行配置文件,不用動其它地方。

看到這兒,你是否是想起某些開源框架?好比,Spring 的依賴注入。

// 加上一行註解,Spring 就接管這個類的建立工做
@Service
public class UserService {
    // 省略業務代碼...
}

你在某個類上加一行註解(這至關於加一行配置),Spring 就幫你接管這個類,你不用操心怎麼建立對象了。而 Spring 之因此能接管你這個類,是由於利用了 Java 的反射。

簡單來講,咱們若是用好反射,能減小大量重複的代碼。

接下來,咱們來看看反射能作什麼吧~

反射獲取類信息

若是你想操做一個類,那得知道這個類的信息。好比,有哪些變量,有哪些構造器,有哪些方法...沒有這些信息,你連代碼都無法寫,更別談反射了。

限於篇幅,咱們主要講怎麼獲取類的 Class 對象、成員變量、方法。

Class 對象只有 JVM 才能建立,裏面有一個類的全部信息,包括:成員變量、方法、構造器等等。

換句話說,建立 Class 對象是 JVM 的事,咱們不用管。但想經過反射來操做一個類,得先拿到這個類的 Class 對象,這有三種方式:

1. Class.forName("類的全限定名")
2. 實例對象.getClass()
3. 類名.class

你能夠看下面的代碼:

public class User {

    public static void main(String[] args) throws Exception {
        // 1. Class.forName("類的全限定名")
        Class clazz1 = Class.forName("com.jiarupc.reflection.User");

        // 2. 實例對象.getClass()
        User user = new User();
        Class clazz2 = user.getClass();

        // 3. 類名.class
        Class clazz3 = ClientUser.class;
    }
}

當你經過這三種方式,拿到 Class 對象後,就能夠用反射獲取類的信息了。

Field 對象表明類的成員變量。咱們想拿到一個類的成員變量,能夠調用 Class 對象的四個方法。

1. Field getField(String name)          - 獲取公共字段
2. Field[] getFields()                  - 獲取全部公共字段 
3. Field getDeclaredField(String name)  - 獲取成員變量
4. Field[] getDeclaredFields()          - 獲取全部成員變量

咱們嘗試下獲取全部成員變量,代碼邏輯是這樣的:經過 User 類的全限定名,獲取 Class 對象,而後調用 getDeclaredFields() 方法,拿到 User 類的所有成員變量,最後把變量名、類型輸出到控制檯。

public class User {
    // 惟一標識
    private Long id;
    // 用戶名
    private String username;

    public static void main(String[] args) throws Exception {
        // 獲取類的 Class 對象
        Class clazz = Class.forName("com.jiarupc.reflection.User");

        // 獲取類的全部成員變量
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            String msg = String.format("變量名:%s, 類型:%s", field.getName(), field.getType());
            System.out.println(msg);
        }
    }
}

Method 對象表明類的方法。要拿到一個類的方法,Class 對象一樣提供了四個方法:

1. Method getMethod(String name, Class[] params)            - 經過方法名、傳入參數,獲取公共方法
2. Method[] getMethods()                                    - 獲取全部公共方法 
3. Method getDeclaredMethod(String name, Class[] params)    - 經過方法名、傳入參數,獲取任何方法
4. Method[] getDeclaredMethods()                            - 獲取全部方法

一樣的,咱們嘗試下獲取全部方法,先經過 User 類的全限定名,獲取 Class 對象,而後調用 getDeclaredMethods() 方法,拿到 User 類的所有成員方法,最後把方法名、形參數量輸出到控制檯。

public class User {
    // 惟一標識
    private Long id;
    // 用戶名
    private String username;

    public static void main(String[] args) throws Exception {
        // 獲取類的 Class 對象
        Class clazz = Class.forName("com.jiarupc.reflection.User");

        // 獲取類的全部方法
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            String msg = String.format("方法名:%s, 形參數量:%s", method.getName(), method.getParameterCount());
            System.out.println(msg);
        }
    }
}

看到這兒,你應該能知道:怎麼經過反射獲取類的信息。

首先,獲取類的 Class 對象有三種方式;而後,獲取類的成員變量,這對應着 Field 對象;最後,獲取類的方法,這對應着 Method 對象。

然而,反射不止能拿到類的信息,還能操做類。

反射操做類

反射能玩出不少花樣,但我認爲最重要的是:建立對象和調用方法。

建立對象是一切的前提。對反射來講,若是沒有建立對象,那咱們只能看看這個類的信息。好比,有什麼成員變量,有什麼方法之類的。而若是你想操做一個類,那麼第一步就是建立對象。

你想要建立對象,必須調用類的構造器。這分爲兩種狀況,最簡單的是:你寫了一個類,但沒有寫構造器,那這個類會自帶一個無參的構造器,這就好辦了。

public class User {
    // 惟一標識
    private Long id;
    // 用戶名
    private String username;

    public static void main(String[] args) throws Exception {
        // 獲取類的 Class 對象
        Class clazz = Class.forName("com.jiarupc.reflection.User");

        // 建立對象
        Object object = clazz.newInstance();
        System.out.println(object);
    }
}

咱們先獲取類的 Class 對象,而後調用 newInstance()。

但還有一種狀況,我不用 Java 自帶的構造器,而是本身寫。這種狀況會複雜一些,你得指定傳入參數的類型,先拿到構造器,再調用 newInstance() 方法。

public class User {
    // 惟一標識
    private Long id;
    // 用戶名
    private String username;

    // 構造器1
    public User(Long id) {
        this.id = id;
        this.username = null;
    }
    // 構造器2
    public User(Long id, String username) {
        this.id = id;
        this.username = username;
    }
    
    public static void main(String[] args) throws Exception {
        // 獲取類的 Class 對象
        Class clazz = Class.forName("com.jiarupc.reflection.User");

        // 經過傳入參數,獲取構造器,再建立對象
        Constructor constructor = clazz.getConstructor(Long.class, String.class);
        Object object = constructor.newInstance(1L, "jiarupc");
        System.out.println(object);
    }
}

咱們要在一開始就設置 id 和 username,那麼你得傳入參數的類型,先找到構造器2-constructor;而後,傳入 id 和 username 到 constructor.newInstance() 方法,就能獲得一個用戶對象。

當拿到構造器,並建立好對象後,咱們就能夠調用對象的方法了。

調用對象的方法分爲兩步:第一步,找到方法;第二步,調用方法。這聽起來是很是簡單,事實上也很是簡單。你能夠看下面的代碼。

public class User {
    // 惟一標識
    private Long id;
    // 用戶名
    private String username;
    
    // ..忽略 set/get 方法
    
    public static void main(String[] args) throws Exception {
        // 獲取類的 Class 對象
        Class clazz = Class.forName("com.jiarupc.reflection.User");

        // 建立對象
        Object object = clazz.newInstance();
        System.out.println(object);
        
        // 經過方法名、傳入參數,找到方法-setUsername
        Method method = clazz.getMethod("setUsername", String.class);
        
        // 調用 object 對象的 setUsername() 方法
        method.invoke(object, "JerryWu");
        
        // 輸出全部成員變量
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            String msg = String.format("變量名:%s, 變量值:%s", field.getName(), field.get(object));
            System.out.println(msg);
        }
    }
}

咱們經過方法名-setUsername、參數類型-String,找到 setUsername 方法;而後,傳入參數-username 到 method.invoke(),執行setUsername 方法;最後,輸出全部成員變量,驗證一下結果。

寫在最後

反射是一種動態操做類的機制,它有兩個用處。

第一個用處,經過反射,咱們能夠拿到一個類的信息,包括:成員變量、方法、構造器等等。

第二個用處,經過反射,咱們能夠操做一個類,包括:建立對象、調用對象的方法、修改對象的成員變量。

由於框架要以同一套算法,來應對不一樣的數據結構。因此,開源框架大量用到了反射。好比,Spring 的依賴注入就離不開反射。

相關文章
相關標籤/搜索