亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

Java 反射

本小節我們來學習一個 Java 語言中較為深入的概念 —— 反射(reflection),很多小伙伴即便參與了工作,可能也極少用到 Java 反射機制,但是如果你想要開發一個 web 框架,反射是不可或缺的知識點。本小節我們將了解到 什么是反射,反射的使用場景,不得不提的 Class 類,如何通過反射訪問類內部的字段、方法以及構造方法等知識點。

1. 什么是反射

Java 的反射(reflection)機制是指在程序的運行狀態中,可以構造任意一個類的對象,可以了解任意一個對象所屬的類,可以了解任意一個類的成員變量和方法,可以調用任意一個對象的屬性和方法。這種動態獲取程序信息以及動態調用對象的功能稱為 Java 語言的反射機制。反射被視為動態語言的關鍵。

通常情況下,我們想調用一個類內部的屬性或方法,需要先實例化這個類,然后通過對象去調用類內部的屬性和方法;通過 Java 的反射機制,我們就可以在程序的運行狀態中,動態獲取類的信息,注入類內部的屬性和方法,完成對象的實例化等操作。

概念可能比較抽象,我們來看一下結合示意圖看一下:

圖中解釋了兩個問題:

  1. 程序運行狀態中指的是什么時刻Hello.java 源代碼文件經過編譯得到 Hello.class 字節碼文件,想要運行這個程序,就要通過 JVM 的 ClassLoader (類加載器)加載 Hello.class,然后 JVM 來運行 Hello.class,程序的運行期間指的就是此刻;
  2. 什么是反射,它有哪些功能:在程序運行期間,可以動態獲得 Hello 類中的屬性和方法、動態完成 Hello 類的對象實例化等操作,這個功能就稱為反射。

說到這里,大家可能覺得,在編寫代碼時直接通過 new 的方式就可以實例化一個對象,訪問其屬性和方法,為什么偏偏要繞個彎子,通過反射機制來進行這些操作呢?下面我們就來看一下反射的使用場景。

2. 反射的使用場景

Java 的反射機制,主要用來編寫一些通用性較高的代碼或者編寫框架的時候使用。

通過反射的概念,我們可以知道,在程序的運行狀態中,對于任意一個類,通過反射都可以動態獲取其信息以及動態調用對象。

例如,很多框架都可以通過配置文件,來讓開發者指定使用不同的類,開發者只需要關心配置,不需要關心代碼的具體實現,具體實現都在框架的內部,通過反射就可以動態生成類的對象,調用這個類下面的一些方法。

下面的內容,我們將學習反射的相關 API,在本小節的最后,我將分享一個自己實際開發中的反射案例。

3. 反射常用類概述

學習反射就需要了解反射相關的一些類,下面我們來看一下如下這幾個類:

  • ClassClass 類的實例表示正在運行的 Java 應用程序中的類和接口;
  • Constructor:關于類的單個構造方法的信息以及對它的權限訪問;
  • Field:Field 提供有關類或接口的單個字段的信息,以及對它的動態訪問權限;
  • Method:Method 提供關于類或接口上單獨某個方法的信息。

字節碼文件想要運行都是要被虛擬機加載的,每加載一種類,Java 虛擬機都會為其創建一個 Class 類型的實例,并關聯起來。

例如,我們自定義了一個 ImoocStudent.java 類,類中包含有構造方法、成員屬性、成員方法等:

public class ImoocStudent {
    // 無參構造方法
    public ImoocStudent() {
    }

    // 有參構造方法
    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 會被編譯器編譯成字節碼文件 ImoocStudent.class,當 Java 虛擬機加載這個 ImoocStudent.class 的時候,就會創建一個 Class 類型的實例對象:

Class cls = new Class(ImoocStudent);

JVM 為我們自動創建了這個類的對象實例,因此就可以獲取類內部的構造方法、屬性和方法等 ImoocStudent 的構造方法就稱為 Constructor,可以創建對象的實例,屬性就稱為 Field,可以為屬性賦值,方法就稱為 Method,可以執行方法。

4. Class 類

4.1 Class 類和 class 文件的關系

java.lang.Class 類用于表示一個類的字節碼(.class)文件。

4.2 獲取 Class 對象的方法

想要使用反射,就要獲取某個 class 文件對應的 Class 對象,我們有 3 種方法:

  1. 類名.class:即通過一個 Class 的靜態變量 class 獲取,實例如下:
Class cls = ImoocStudent.class;
  1. 對象.getClass ():前提是有該類的對象實例,該方法由 java.lang.Object 類提供,實例如下:
ImoocStudent imoocStudent = new ImoocStudent("小慕");
Class imoocStudent.getClass();
  1. Class.forName (“包名。類名”):如果知道一個類的完整包名,可以通過 Class 類的靜態方法 forName() 獲得 Class 對象,實例如下:
class cls = Class.forName("java.util.ArrayList");

4.3 實例

package com.imooc.reflect;

public class ImoocStudent {
    // 無參構造方法
    public ImoocStudent() {
    }

    // 有參構造方法
    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:對象.getClass()
        ImoocStudent student = new ImoocStudent();
        Class cls2 = student.getClass();

        // 方法3:Class.forName("包名.類名")
        Class cls3 = Class.forName("com.imooc.reflect.ImoocStudent");
    }

}

代碼中,我們在 com.imooc.reflect 包下定義了一個 ImoocStudent 類,并在主方法中,使用了 3 種方法獲取 Class 的實例對象,其 forName() 方法會拋出一個 ClassNotFoundException。

4.4 調用構造方法

獲取了 Class 的實例對象,我們就可以獲取 Contructor 對象,調用其構造方法了。

那么如何獲得 Constructor 對象?Class 提供了以下幾個方法來獲取:

  • Constructor getConstructor(Class...):獲取某個 public 的構造方法;
  • Constructor getDeclaredConstructor(Class...):獲取某個構造方法;
  • Constructor[] getConstructors():獲取所有 public 的構造方法;
  • Constructor[] getDeclaredConstructors():獲取所有構造方法。

通常我們調用類的構造方法,這樣寫的(以 StringBuilder 為例):

// 實例化StringBuilder對象
StringBuilder name = new StringBuilder("Hello Imooc");

通過反射,要先獲取 Constructor 對象,再調用 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 {
        // 獲取構造方法
        Constructor constructor = StringBuffer.class.getConstructor(String.class);
        // 調用構造方法
        Object str = constructor.newInstance("Hello Imooc");
        System.out.println(str);
    }
}
運行案例 點擊 "運行案例" 可查看在線運行效果

運行結果:

Hello Imooc

5. 訪問字段

前面我們知道了如何獲取 Class 實例,只要獲取了 Class 實例,就可以獲取它的所有信息。

5.1 獲取字段

Field 類代表某個類中的一個成員變量,并提供動態的訪問權限。Class 提供了以下幾個方法來獲取字段:

  • Field getField(name):根據屬性名獲取某個 public 的字段(包含父類繼承);
  • Field getDeclaredField(name):根據屬性名獲取當前類的某個字段(不包含父類繼承);
  • Field[] getFields():獲得所有的 public 字段(包含父類繼承);
  • Field[] getDeclaredFields():獲取當前類的所有字段(不包含父類繼承)。

獲取字段的實例如下:

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 實例
        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());
        }
    }

}

運行結果:

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 個屬性,其中 position 為公有屬性,nicknamebalance 為私有屬性。我們通過類名.class 的方式獲取了 Class 實例,通過調用其實例方法并打印其返回結果,驗證了獲取字段,獲取單個字段方法,在沒有找到該指定字段的情況下,會拋出一個 NoSuchFieldException。

調用獲取所有字段方法,返回的是一個 Field 類型的數組??梢哉{用 Field 類下的 getName() 方法來獲取字段名稱,getType() 方法來獲取字段類型。

5.2 獲取字段值

既然我們已經獲取到了字段,那么就理所當然地可以獲取字段的值??梢酝ㄟ^ Field 類下的 Object get(Object obj) 方法來獲取指定字段的值,方法的參數 Object 為對象實例,實例如下:

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 {
        // 實例化一個 ImoocStudent2 對象
        ImoocStudent2 imoocStudent2 = new ImoocStudent2("小慕", "架構師");
        Class cls = imoocStudent2.getClass();
        Field position = cls.getField("position");
        Object o = position.get(imoocStudent2);
        System.out.println(o);
    }

}

運行結果:

架構師

ImoocStudent2 內部分別包含一個公有屬性 position 和一個私有屬性 nickname,我們首先實例化了一個 ImoocStudent2 對象,并且獲取了與其對應的 Class 對象,然后調用 getField() 方法獲取了 position 字段,通過調用 Field 類下的實例方法 Object get(Object obj) 來獲取了 position 字段的值。

這里值得注意的是,如果我們想要獲取 nickname 字段的值會稍有不同,因為它是私有屬性,我們看到 get() 方法會拋出 IllegalAccessException 異常,如果直接調用 get() 方法獲取私有屬性,就會拋出此異常。

想要獲取私有屬性,必須調用 Field.setAccessible(boolean flag) 方法來設置該字段的訪問權限為 true,表示可以訪問。在 main() 方法中,獲取私有屬性 nickname 的值的實例如下:

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
    // 實例化一個 ImoocStudent2 對象
    ImoocStudent2 imoocStudent2 = new ImoocStudent2("小慕", "架構師");
    Class cls = imoocStudent2.getClass();
    Field nickname = cls.getDeclaredField("nickname");
    // 設置可以訪問
    nickname.setAccessible(true);
    Object o = nickname.get(imoocStudent2);
    System.out.println(o);
}

此時,就不會拋出異常,運行結果:

小慕

5.2 為字段賦值

為字段賦值也很簡單,調用 Field.set(Object obj, Object value) 方法即可,第一個 Object 參數是指定的實例,第二個 Object 參數是待修改的值。我們直接來看實例:

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 {
        // 實例化一個 ImoocStudent3 對象
        ImoocStudent3 imoocStudent3 = new ImoocStudent3("小慕");
        Class cls = imoocStudent3.getClass();
        Field nickname = cls.getDeclaredField("nickname");
        nickname.setAccessible(true);
        // 設置字段值
        nickname.set(imoocStudent3, "Colorful");
        // 打印設置后的內容
        System.out.println(imoocStudent3.getNickname());
    }

}

運行結果:

Colorful

6. 調用方法

Method 類代表某一個類中的一個成員方法。

6.1 獲取方法

Class 提供了以下幾個方法來獲取方法:

  • Method getMethod(name, Class...):獲取某個 public 的方法(包含父類繼承);
  • Method getgetDeclaredMethod(name, Class...):獲取當前類的某個方法(不包含父類);
  • Method[] getMethods():獲取所有 public 的方法(包含父類繼承);
  • Method[] getDeclareMethods():獲取當前類的所有方法(不包含父類繼承)。

獲取方法和獲取字段大同小異,只需調用以上 API 即可,這里不再贅述。

6.2 調用方法

獲取方法的目的就是調用方法,調用方法也就是讓方法執行。

通常情況下,我們是這樣調用對象下的實例方法(以 String 類的 replace() 方法為例):

String name = new String("Colorful");
String result = name.replace("ful", "");

改寫成通過反射方法調用:

實例演示
預覽 復制
復制成功!
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionDemo1 {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 實例化字符串對象
        String name = new String("Colorful");
        // 獲取 method 對象
        Method method = String.class.getMethod("replace", CharSequence.class, CharSequence.class);
        // 調用 invoke() 執行方法
        String result = (String) method.invoke(name,  "ful", "");
        System.out.println(result);
    }
}
運行案例 點擊 "運行案例" 可查看在線運行效果

運行結果:

Color

代碼中,調用 Method 實例的 invoke(Object obj, Object...args) 方法,就是通過反射來調用了該方法。

其中 invoke() 方法的第一個參數為對象實例,緊接著的可變參數就是要調用方法的參數,參數要保持一致。

7. 反射應用

Tips: 理解此部分內容可能需要閱讀者有一定的開發經驗

學習完了反射,大家可能依然非常疑惑,反射似乎離我們的實際開發非常遙遠,實際情況也的確是這樣的。因為我們在實際開發中基本不會用到反射。下面我來分享一個實際開發中應用反射的案例。

場景是這樣的:有一個文件上傳系統,文件上傳系統有多種不同的方式(上傳到服務器本地、上傳到七牛云、阿里云 OSS 等),因此就有多個不同的文件上傳實現類。系統希望通過配置文件來獲取用戶的配置,再去實例化對應的實現類。因此,我們一開始的思路可能是這樣的(偽代碼):

public class UploaderFactory {
    
    // 通過配置文件獲取到的配置,可能為 local(上傳到本地) qiniuyun(上傳到七牛) 
    private String uploader;
    
    // 創建實現類對象的方法
    public Uploader createUploader() {
        switch (uploader) {
            case "local":
                // 實例化上傳到本地的實現類
                return new LocalUploader();
            case "qiniuyun":
                // 實例化上傳到七牛云的實現類
                return new QiniuUploader();
            default:
                break;
        }
        return null;
    }
}

createUploader() 就是創建實現類的方法,它通過 switch case 結構來判斷從配置文件中獲取的 uploader 變量。

這看上去似乎沒有什么問題,但試想,后續我們的實現類越來越多,就需要一直向下添加 case 語句,并且要約定配置文件中的字符串要和 case 匹配才行。這樣的代碼既不穩定也不健壯。

換一種思路考慮問題,我們可以通過反射機制來改寫這里的代碼。首先,約定配置文件的 uploader 配置項不再是字符串,改為類的全路徑命名。因此,在 createUploader() 方法中不再需要 switch case 結構來判斷,直接通過 Class.forName(uploader) 就可以獲取 Class 實例,并調用其構造方法實例化對應的文件上傳對象,偽代碼如下:

public class UploaderFactory {
    
    // 通過配置文件獲取到的配置,實現類的包名.類名
    private String uploader;
    
    // 創建實現類對象的方法
    public Uploader createUploader() {
        // 獲取構造方法
		Constructor constructor = Class.forName(uploader).getConstructor();
        return (Uploader) constructor.newInstance();
    }
}

通過反射實例化對應的實現類,我們不需要再維護 UploaderFactory 下的代碼,其實現類的命名、放置位置也不受約束,只需要在配置文件中指定類名全路徑即可。

8. 小結

通過本小節的學習,我們知道了反射是 Java 提供的一種機制,它可以在程序的運行狀態中,動態獲取類的信息,注入類內部的屬性和方法,完成對象的實例化等操作。獲取 Class 對象有 3 種方法,通過學習反射的相關接口,我們了解到通過反射可以實現一切我們想要的操作。在本小節的最后,我也分享了一個我在實際開發中應用反射的案例,希望能對大家有所啟發。