Java中的反射機制詳解

文章目錄

一、簡介

  • 在Java 運行時環境中,對於任意一個類,能否知道這個類有哪些屬性和方法?
  • 對於任意一個對象,能否調用它的任意一個方法?
  • 答案是 肯定的
  • 這種動態的獲取類的信息以及動態調用對象的方法的功能都來自於Java語言的反射機制

1.Java反射機制提供的功能

  • 運行時判斷任意一個對象所屬的類
  • 運行時構造任意一個類的對象
  • 運行時判斷任意一個類所具有的成員變量和方法
  • 運行時調用任意一個對象的方法

2反射讓Java具有動態語言的性質

  • Reflection是Java被視爲動態(準動態)語言的一個關鍵性質。這個機制允許程序在運行時透過Reflection APIs取得任何一個已知名稱class的內部信息,其中包括
    • modifiers:public/static
    • superclass:Object
    • 實現的接口:Serializable
    • 也包括fieldsmethods的所有信息
    • 並且可以在運行時改變fields內容或調用methods包括私有變量和私有方法。也就說類的所有包裝機制在反射機制面前都失效了
  • 一般認爲動態語言的定義是:程序運行時,允許改變程序結構或變量類型,這種語言稱爲動態語言,從這個觀點看Python,JavaScript是動態語言,Java,C/C++是靜態語言
  • 雖然從嚴格定義來看,Java是一門靜態語言,但是反射機制讓Java具有了動態相關的機制
  • Reflection用在Java身上表示我們可以於 運行時加載、探知、使用編譯期完全未知的classes
  • 也就是說,Java可以加載一個運行時才得知名稱的class,獲取其完整的構造,( 但不包括methond定義),並生成其對象實體、或對其fields設值、喚起器methods
  • 這種“看透class”的能力又被稱爲introspection(內觀/反省)。(Reflectionintrospection)是常被並提的兩個術語

3.java.lang.reflect(關於反射的API介紹)

JDK中主要由以下的類來實現反射機制,這些類大都在java.lang.reflect

  • Class類:代表一個類(位於java.lang下)
    • 代表類本身,每一個類都有一個與之對應的Class
  • Field類:代表類的成員變量/屬性
  • Method類:代表類的方法
  • Constructor類:代表類的構造方法
  • Array類:提供了動態創建數組,以及訪問數組的元素的靜態方法

3.1基礎示例:如何通過反射獲取一個類的所有方法?

  • 代碼
   public static void main(String[] args) throws ClassNotFoundException {
        //一定要理解到反射是一個運行期的行爲
        //在下面中 Class.forName("java.lang.String");
        // 中的內容在編譯期只是一個字符串而已,不代表任何實際意義
        //注意 這裏的參數需要是 類的全限定名
        Class<?> classType = Class.forName("java.lang.String");
        //獲取該類的所有方法
        Method[] methods = classType.getDeclaredMethods();
        for (Method method : methods) {
            System.out.println(method);
        }
    }
  • 輸出:可以獲取到該類的所有方法,包括私有方法
    在這裏插入圖片描述

3.1.1 一定要理解反射是運行期的行爲(重要)!!

  • 例1
    在這裏插入圖片描述- 例2
    在這裏插入圖片描述

3.2通過反射調用類中的方法

最常見的反射的使用,一定要掌握和理解每一行代碼的意思

  • 代碼
    • 一個類中定義了兩個帶參數的方法,想要通過反射機制來調用這些方法
    • 下面的代碼是一般的調用步驟
public class Demo03_InvokTester {

    public int add(int a, int b){
        return a +b;
    }

    public String out(String str){
        return "hello:" + str;
    }


    public static void main(String[] args) throws Exception {

        //1.首先獲取類對應的Class對象
        Class<?> classType = Demo03_InvokTester.class;
        //2.根據Class對象獲取對應類的 實例
        Object invokeTester = classType.newInstance();
        /*3.獲取方法對象: 根據Class類 獲取方法對象,這個方法對象 是和該類中 的某個方法是一一對應的
             3.1前面展示了獲取類的所有的方法
             3.2這裏是獲取某個具體的方法
             3.3參數解讀:
                "add" :方法名 new Class[]{int.class,int.class}:該方法參數列表 中每一個參數的數據類型對應的Class類 組成的Class對象數組
             3.4 爲何需要兩個參數?因爲方法重載的存在,無法通過方法名來唯一的確定一個方法,只有方法名+參數類型+參數個數
              才能唯一的確定一個方法
             3.5那麼最終的返回值就是 獲取classType對應類中的名爲add且參數列表爲兩個整型的方法 對象
        */
        Method addMethod = classType.getMethod("add", new Class[]{int.class,int.class});
        /*4.方法調用: 通過方法對象的invoke方法  來調用類中的方法
            4.1invokeTester: 類對應的Class對象,在哪個對象上調用方法
            4.2new Object[]{1,2}:傳入方法的參數
            4.3注意invoke的返回值是一個Object對象,後面需要強制轉換爲真正的對象
            4.4最終的返回值就是該方法運行後的返回值
            4.5注意和上述獲取方法時參數的異同
                上面的new Class[]{int.class,int.class}可以認爲是一個抽象的,描述類型的信息,可認爲是形參
                這裏的new Object[]{1,2} 在方法調用的時候當然需要一個具體的參數信息,可以認爲是實參
                這兩個數組的長度一定是一樣的

         */

        Object res = addMethod.invoke(invokeTester, new Object[]{1,2});
        System.out.println((Integer)res);

        System.out.println("-------------------------------");
        //下面用同樣的方法來調用out方法
        //注意這裏就算方法只有一個參數 這裏也仍然需要傳入Class對象數組
        Method outMethod = classType.getMethod("out", new Class[]{String.class});

        Object out = outMethod.invoke(invokeTester, new Object[]{"world"});

        System.out.println((String)out);
    }
}

3.3類,Class類,對象之間的關係是?

先記住,後理解

Java中,無論生成某個類的多少個對象,這些對象都對應於同一個Class對象

二、深入理解Class類,Method類,Field類

Class類對應 類,Class類是反射的基礎,它包含了這個類在運行時的所有想要的信息
Method類 對應類中的一個方法
Field類 對應類中的一個屬性

1.獲取某個類/對象所對應的Class對象常用的3中方式(非常重要)

  • 1.使用Class類的靜態方法forname
 Class<?> classType = Class.forName("java.lang.String");
  • 2.使用類的.class語法:類.class
  Class<?> classType = String.class;
  • 3.使用對象的getClass()方法:
    • 既然是通過對象來獲取的,那就一定先要有對象
String s = "aa";
Class<?> clazz = s.getClass();

2.通過Class類對象來獲取實例(對象)的2種情況

在反射中,第一步一定是獲取某類/對象對應的Class對象,第二步一般就是通過Class對象獲取某類的實例(對象),在通過Class對象獲取某類的實例的時候會有兩種情況,根據該類是否提供無參的構造方法。

2.1通過類的不帶參數的構造方法來生成對象的2種方式

  • 1.獲取Class對象,直接通過該Class對象的newInstance()方法獲取實例
Class<?> classType = String.class;
Object obj = classType.newInstance();
  • 2.先獲取Class對象,再通過Class對象來獲取對應的Constructor對象,再通過Constructor對象的newInstance()方法生成
//1.獲取類對象的Class對象
Class<?> classType = object.getClass();
/2.現在需要通過Class對象 獲取該對象的實例
Constructor cons =  classType.getConstructor(new Class[]{});
Object obj = cons.newInstance(new Object[]{});
  • 在這種通過類的不帶參數的構造方法獲取實例的時候,上述兩種方法是等價的,而且方法1更爲簡便
  • 但是如果某類沒有不帶參數的構造方法或者說有時候必須使用帶構造參數的方法來獲取實例,就只能使用方法2了

2.2若想通過類的帶參數的構造方法生成對象,只能用1種方式

這種方式就是上述的方式2

  • 核心步驟
//1.獲取類對象的Class對象
        Class<?> classType = object.getClass();
        //2.現在需要通過Class對象 獲取該對象的實例
        //2.1通過Class對象先獲取 Constructor類,注意參數列表的傳入
        Constructor cons =  classType.getConstructor(new Class[]{String.class,int.class});
        //2.2調用Constructor類的newInstance獲取實例
        Object obj = cons.newInstance(new Object[]{"hello",13});
  • 完整示例代碼

public class ReflectTester {

    public static void main(String[] args) throws Exception {
        ReflectTester tester = new ReflectTester();
        tester.copy(new Customer("a",1));
    }

    //該方法實現對Customer對象的拷貝
    public Object copy(Object object) throws Exception {
        //1.獲取類對象的Class對象
        Class<?> classType = object.getClass();
        //2.現在需要通過Class對象 獲取該對象的實例
        //2.1通過Class對象先獲取 Constructor類,注意參數列表的傳入
        Constructor cons =  classType.getConstructor(new Class[]{String.class,int.class});
        //2.2調用Constructor類的newInstance獲取實例
        Object obj = cons.newInstance(new Object[]{"hello",13});
        System.out.println(obj);
        return obj;
    }

}
class Customer{

    private Long id;
    private String name;
    private int age;

    public Customer(String name, int age){
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

2.2.1如果在這種情況下使用Class類的newInstance()方法來獲取實例,在運行時會報錯

  • 代碼:只需要修改上述copy方法爲下面
 public Object copy(Object object) throws Exception {
        //1.獲取類對象的Class對象
        Class<?> classType = object.getClass();
        //通過Class對象的newInstance()方法獲取實例
        Object obj = classType.newInstance();
        System.out.println(obj);
        return obj;
    }
  • 可以觀察到在編譯期是沒有任何報錯的
    在這裏插入圖片描述- 運行結果

在這裏插入圖片描述

2.3API文檔解讀

上面的內容更多的是從結果直接說出來了,下面嘗試從API文檔,看看對相應方法的描述

  • Class.newInstance
    在這裏插入圖片描述
  • Constructor.newInstance

在這裏插入圖片描述

3.利用反射完成對象的拷貝

接着上述的例子,利用反射完成對一個Customer對象的反射

3.1代碼

認真理解代碼中的每一行,特別是copy函數

public class ReflectTester {
    public static void main(String[] args) throws Exception {
        ReflectTester tester = new ReflectTester();
        Customer c0 = new Customer("xpt",18);
        //注意id類型是Long
        c0.setId(1L);
        Customer c1 = (Customer) tester.copy(c0);
        System.out.println("被拷貝對象:" + c0);
        System.out.println("拷貝目標對象:" + c1);
    }

    //該方法實現對Customer對象的拷貝
    public Object copy(Object object) throws Exception {
        //1.獲取 被拷貝對象的Class對象
        Class<?> classType = object.getClass();
        //2.通過classType實例化一個 拷貝 目標對象
        Object objectCopy = classType.getConstructor(new Class[]{}).newInstance(new Object[]{});

        //3.獲取 對象的所有成員變量
        Field[] fields = classType.getDeclaredFields();

        //4.遍歷成員變量 逐一拷貝
        for (Field field : fields) {
            //4.1獲取成員變量的名字(Field類提供的方法)
            String name = field.getName();

            /*4.2獲取 每一個成員變量名 對應的getter/setter方法
                4.2.1將屬性名的第一個字母大寫
                4.2.2 這是因爲getter/和setter方法的規律性決定的
                4.2.3 然後拼接字符串
            */
            String firstLetter = name.substring(0,1).toUpperCase();
            String getMethodName = "get" + firstLetter + name.substring(1);
            String setMethodName = "set" + firstLetter + name.substring(1);

            //4.3獲取Method 對象(get方法參數列表爲空)
            Method getMethod = classType.getMethod(getMethodName, new Class[]{});
            //4.3.1 set方法參數就是成員變量,而這個成員變量 剛剛就是 field,獲取它的類型即可
            Method setMethod = classType.getMethod(setMethodName, new Class[]{field.getType()});

            //4.4調用get方法 獲取值
            Object value = getMethod.invoke(object, new Object[]{});
            //4.5調用set方法 爲 拷貝目標對象賦值
            setMethod.invoke(objectCopy, new Object[]{value});
        }
        //5.完成拷貝  返回拷貝目標對象即可
        return objectCopy;
    }

}
class Customer{

    private Long id;
    private String name;
    private int age;

    public Customer(){

    }
    public Customer(String name, int age){
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

3.2結果

在這裏插入圖片描述

三、java.lang.Array類

提供了動態創建和訪問數組元素的各種靜態方法

1.Array類的基本使用

創建Array實例,調用方法

  • 注意基本方法的使用即可

public class ArrayTest1 {

    public static void main(String[] args) throws ClassNotFoundException {

        //1.獲取Sting類的Class對象
        Class<?> classType = Class.forName("java.lang.String");
        //2.新建array數組 長度爲10的字符串數組
        Object array = Array.newInstance(classType, 10);
        //3.爲數組的指定index賦予指定的值
        Array.set(array, 5, "hello");
        String str = (String) Array.get(array, 5);
        System.out.println(str);

    }

}

2 Integer.TYPE和Integer.class的區別?

  • Integer.TYPE返回int
  • Integer.class返回的是Integer類Class對象
    在這裏插入圖片描述

3.Arrays創建多維數組/爲多維數組中的具體位置賦值以及理解數組的組件類型

  public static void main(String[] args) throws Exception {

//        System.out.println(Integer.TYPE);
//        System.out.println(Integer.class);

        int[] dims = new int[]{5,10,15};
        /*1. 創建多維數組對象
          1.1 Integer.TYPE:數組元素類型
          1.2 dims 規定了數組的形狀,dims本身是個數組,這個數組的長度代表 數組array的維度,數組的每一個元素代表每一個維度的長度
          1.3 簡單的說就是  這個句話創建了一個 5 * 10 * 15的三維數組
        */
        Object array = Array.newInstance(Integer.TYPE, dims);
        System.out.println(array instanceof int[][][]);

        /*
            2.獲取數組的組件類型
            2.1數組的組件類型就是: 二維數組 是有 多個一維數組構成的 那二維數組的組件類型就是一維數組
                                 三維數組 是由 二維數組構成的    三維數組的組件類型就是二維數組
            2.2 array是一個三維數組,那它的組件類型肯定就是一個二維數組
         */
        Class<?> classType = array.getClass().getComponentType();
        //3.獲取三維數組array中index = 3的元素,顯然是個二維數組
        Object arrayObj = Array.get(array, 3);
        System.out.println(arrayObj instanceof int[][]);
        //4.獲取二維數組 arrayObj的index = 5的元素 , 當然是一個一維數組
        arrayObj = Array.get(arrayObj, 5);
        System.out.println( arrayObj instanceof int[]);
        //5.給一維數組 arrayObj的index = 10賦值37
        Array.setInt(arrayObj, 10, 37);

        //6.把最開始的array數組強轉爲三維整型數組
        int[][][] arrayCast = (int[][][]) array;
        //7.這個值就是賦值給它的37
        System.out.println(arrayCast[3][5][10]);
        
    }

四、利用反射機制調用對象的私有方法,訪問對象的私有成員變量

1.如何獲取對象的私有方法?

  • 查找Class類的相關文檔,發現和獲取方法對象有關的方法,我們閱讀文檔,比較一下各自的區別

在這裏插入圖片描述

  • 從文檔中可以很清楚的看到獲取方法對象的相關方法
  • 那麼顯然想要獲取到某個特定的私有方法,需要用:
    • getDeclaredMethod(String name, Class<?>... parameterTypes)

2.如何調用私有方法?(錯誤示例以及分析)

先按照我們前面學習過的調用對象的方法,看會發生什麼

  • 先定義一個類以及提供一個私有方法
public class PrivateTest {
    private String sayHello(String name){

        return "hello: " + name;
    }
}
  • 創建一個類,目標是調用上述的私有方法
public class TestPrivateMethod {

    public static void main(String[] args) throws Exception {
        //1.創建對象
        PrivateTest p = new PrivateTest();
        //2.獲取對象p對應的Class對象
        Class<?> classType = p.getClass();
        //3.獲取方法對象
        Method method = classType.getDeclaredMethod("sayHello", new Class[]{String.class});
        //4.調用方法
        String str = (String) method.invoke(classType, new Object[]{"world"});
        System.out.println(str);
    }
}
  • 運行結果:
    在這裏插入圖片描述- 錯誤信息分析

在這裏插入圖片描述

  • 這裏提示我們無法訪問該私有方法
  • 但是反射不是無所不能嗎?問題出在哪裏呢?
  • 閱讀文檔
    在這裏插入圖片描述在這裏插入圖片描述

3.調用私有方法(正確示例)

  • 代碼
public class TestPrivateMethod {

    public static void main(String[] args) throws Exception {
        //1.創建對象
        PrivateTest p = new PrivateTest();
        //2.獲取對象p對應的Class對象
        Class<?> classType = p.getClass();
        //3.獲取方法對象
        Method method = classType.getDeclaredMethod("sayHello", new Class[]{String.class});
        //4.設置 以壓制Java的訪問控制檢查
        method.setAccessible(true);
        //5.調用方法
        String str = (String) method.invoke(p, new Object[]{"world"});
        System.out.println(str);
    }
}
  • 結果
    在這裏插入圖片描述

  • 重點
    在這裏插入圖片描述

4.反射調用私有方法小結

  • 1.要能獲取到私有方法對象:getDeclaredMethod(String name, Class<?>... parameterTypes)
  • 2.要設置壓制Java的訪問檢查:method.setAccessible(true);

5.反射對象反問私有成員變量

理解了如何訪問私有方法,那訪問私有對象也很容易了,只是這裏需要使用Field類來完成相關操作

  • 含有私有屬性的類
public class PrivateTest {

    String name = "xpt";

    public String getName() {
        return name;
    }

    private String sayHello(String name){

        return "hello: " + name;
    }
}
  • 利用放射訪問私有屬性
public class TestPrivateField {

    public static void main(String[] args) throws Exception {
        //1.創建對象
        PrivateTest p = new PrivateTest();
        //2.獲取Class對象
        Class<?> classType = p.getClass();
        //3.獲取特定 私有成員變量 類
        Field field = classType.getDeclaredField("name");
        //4.壓制Java語言的訪問檢查
        field.setAccessible(true);
        //5.調用Field類的set方法
        field.set(p, "abc");
        System.out.println(p.getName());
    }

}
  • 結果
    在這裏插入圖片描述

6.反射訪問私有成員變量小結

  • 1…獲取特定 私有成員變量 類 Field field = classType.getDeclaredField("name");
  • 2.壓制Java語言的訪問檢查 field.setAccessible(true);
    在這裏插入圖片描述

6.1一個經典的題目

  • 提供了下述的類和屬性以及方法,問是否能夠改變屬性的初始值
public class PrivateTest {

    String name = "hello";

    public String getName() {
        return name;
    }
}
  • 通過上面如何訪問私有成員變量的分析,這裏應該很容易回答以及如何實現

五、反射知識點總結

前面的內容側重於細節的講解,以及API文檔的閱讀,這裏對前面的內容做一個簡單清晰的總結

1.獲取Class對象的三種方式?

2.如何獲取所有的成員變量?如何獲取某個特定的成員變量?某個特定的私有成員變量?如何訪問某個特定的私有私有成員變量?

3.如何獲取所有的構造函數?如何獲取某個特定的構造函數?私有構造函數?

  • 獲取所有構造方法的getDeclaredConstructors
  • 獲取公有構造方法的 getConstructors

4.如何獲取所有的方法對象?如何獲取特定方法對象?私有方法?如何調用方法?如何調用私有方法?

5.如何實例化一個對象?有幾種情況?

6.常用API的命名規律

  • 上述的內容本質上都是API的使用,並且這些API都是非常有規律的,一看名字就明白含義
  • s結尾的表示複數,返回值都是複數
  • Declared表示獲取聲明的方法,也就是所有的方法/屬性。
  • 不帶s表示訪問某個具體方法/屬性:需要傳入方法名,參數列表類型
  • 根據下面的API應該很容易識別各自的作用和需要傳遞的參數
classType.getConstructors();
        classType.getDeclaredConstructors();

        classType.getMethods();
        classType.getDeclaredMethods();

        classType.getFields();
        classType.getDeclaredMethods();

        classType.getMethod(, );
        classType.getDeclaredMethod();
        classType.getConstructor();
        classType.getDeclaredConstructor();
        classType.getFields();
        classType.getDeclaredField();
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章