Java基礎(十八)反射

diamond_lin發表於2019-03-04

在學習反射之前,我們先來學習一些基本知識。

RTTI

執行時型別識別(RTTI, Run-Time Type Identification)是Java中非常有用的機制,在Java執行時,RTTI維護類的相關資訊。

為什麼講這個東西呢,因為我們今天的主題——反射,也是一種形式的 RTTI。可能有些同學對 RTTI 有些陌生,其實說白了,就是編譯的時候不知道(或者不需要知道)類的詳細,但是執行的時候需要具體執行程式碼。

這個概念有點抽象,我給大家舉個例子,比如說我們的父類引用指向子類物件,然後呼叫了方法 a(),這個時候是由於 RTTI 來根據子類是否重寫方法 a()來判斷是執行子類的方法 a(),還是執行父類的方法 a()。

可能有同學會說上面這不就是動態繫結麼,沒錯,就是動態繫結,但是這也屬於 RTTI 機制。

再比如,向上轉型、向下轉型、instanceof 這些都屬於RTTI,因為這些都牽涉到執行時類的識別。

而我們用到的反射也屬於 RTTI 機制,但是和傳統的 RTTI 又有一部分割槽別。

傳統的 RTTI 有3種實現方式

  • 向上轉型或向下轉型,在 java 中,向下轉型需要強制型別轉換。
  • CLass 物件(用了 Class 物件,並且只是用 CLass 物件 cast 成指定的類)
  • instanceof

傳統的 RTTI 與反射最主要的區別
最主要的區別在於傳統的 RTTI 在編譯期需要.class檔案,而反射不需要。

Class 類

Class 類又稱“類的類”(Class of classes)。如果說類是物件的抽象和集合的話,那麼 Class 類就是對類的抽象和集合。(認真理解這一句話)

每一個 Class 類的物件代表一個其他的類。比如下面程式中,Class 類的物件 c1代表了 Human 類,c2代表了 Woman 類。

public class ClassTest {

    public static void main(String[] args) {
        Human human = new Human();
        Class c1 = human.getClass();
        System.out.println(c1.getName());

        Human woman = new Woman();
        Class c2 = woman.getClass();
        System.out.println(c2.getName());

    }

}

class Human {

}

class Woman extends Human {

}複製程式碼

列印結果就不貼出來了~~

當我們在呼叫物件的 getClass 方法時,就得到對應 Class 物件的引用。

在 c2中,即使我們將 Women 物件的引用向上轉換為 Human 物件的引用,物件所指向的 Class 類物件依然是 Woman。

Java 中每個物件都有相應的 Class 類物件,因此,我們隨時能通過 Class 物件知道某個物件“真正”所屬的類。無論我們對引用進行怎樣的型別轉換,物件本身所對應的 Class 物件都是同一個。當我們通過某個引用呼叫方法時,Java 總能找到正確的 Class 類中所定義的方法並且執行該 Class 類中的程式碼。由於 Class 物件的存在,Java 不會因為型別的向上轉換而迷失。這就是多型的原理。

獲取Class 類的三種方法

  • 物件.getClass()
  • 類名.class;
  • Class.forName()

Class 類的方法

Class 物件記錄了相應類的資訊,比如類的名字,類所在的包等等。

方法很多,具體方法可以去看 API 文件(在 java.lang包下),這裡我介紹幾個最常用的方法。

  • public String getName()獲取類名
  • public String getPackage()獲取包名
  • public Class getSuperclass()獲取父類 class
  • public Fields[] getFields() 獲取所有公共欄位
  • public Methods[] getMethods()獲取所有公共方法
  • public Annotation[] getAnnotations獲取所有註解
  • public Class[] getClasses() 獲取所有內部類(包含父類)
  • public Constructor[] getConstructors()獲取所有公共構造方法
  • getDeclared***() 獲取所有屬性(包含非公共的)

Class類的載入

當Java建立某個類的物件,比如Human類物件時,Java會檢查記憶體中是否有相應的Class物件。

如果記憶體中沒有相應的Class物件,那麼Java會在.class檔案中尋找Human類的定義,並載入Human類的Class物件。

在Class物件載入成功後,其他Human物件的建立和相關操作都將參照該Class物件。

反射操作的相關類

上面我們看 Class 類的方法的時候返回了以下幾個物件。

  • Constructor
  • Method
  • Field

接下來,我們來看看這幾個類吧~

AccessibleObject

咦,這特麼又是什麼類,說好的Constructor、Method、Field 呢
別急。
AccessibleObject 是這三個物件的基類。它提供了將反射的物件標記為在使用時取消預設 Java 語言訪問控制檢查的能力。對於公共成員、預設(打包)訪問成員、受保護成員和私有成員,在分別使用 Field、Method 或 Constructor 物件來設定或獲取欄位、呼叫方法,或者建立和初始化類的新例項的時候,會執行訪問檢查。

  • setAccessible(boolean flag)
    flag 的值為 true 則指示反射的物件在使用時應該取消 Java 語言訪問檢查,也就是我們所說的暴力反射。不原理不過就是關閉了 Java 語言訪問許可權檢查而已。

  • isAccessible()
    獲取是否需要檢查訪問許可權。

Constructor

Constructor 提供了關於類的單個構造方法的資訊。

方法名 介紹
getAnnotation(Classannotation) 如果存在該元素的指定型別的註解,則返回這個註解
getDeclaredAnnotations() 返回直接存在於此方法上的所有註解
getDeclaringClass() 返回 Class 物件,該物件為此構造方法構造的類
getExceptionTypes() 返回丟擲的異常類的 class列表
getGenericExceptionTypes() 返回丟擲的異常列表
getGenericParameterTypes() 方法引數型別列表
getModifiers() 以 int 型的方式返回訪問許可權
getName() 返回構造方法的名稱
getParameterAnnotations() 返回方法引數的註解列表,由於一個引數可能有多個註解,所以是二維陣列
isVarArgs() 是否帶有可變數量引數
newInstance(Object… initargs) 使用此構造器建立一個例項。

Method

Method 提供關於類或介面上單獨某個方法(以及如何訪問該方法)的資訊。所反映的 方法可能是類方法或例項方法(包括抽象方法)。

方法名 介紹
getAnnotation(Classannotation)
getDeclaredAnnotations()
getDeclaringClass()
getDefaultValue() 返回此 Method 例項表示的註釋成員的預設值
getExceptionTypes()
getGenericExceptionTypes()
getGenericParameterTypes()
getModifiers
getName()
getParameterAnnotations()
isVarArgs()
invoke(Object obj, Object… args) 對帶有指定引數的指定物件呼叫由此 Method 物件表示的底層方法

空白描述同 Constructor

Field

Field 提供有關類或介面的單個欄位的資訊,以及對它的動態訪問許可權。反射的欄位可能是一個類(靜態)欄位或例項欄位。

方法名 介紹
get(Object obj) 返回指定物件上此欄位的值
getAnnotation(Classannotation)
getBoolean(Object obj) 獲取一個靜態或例項 boolean 欄位的值
getByte(Object obj) 獲取一個靜態或例項 type 欄位的值
getChar(Object obj) 獲取 char 型別或另一個通過擴充套件轉換可以轉換為 char 型別的基本型別的靜態或例項的值
getDeclaredAnnotation()
getDeclaringClass()
getDouble(Object obj) 同上
getFloat(Object obj) 同上
getGenericType() 返回一個 Type 物件,它表示此 Field 物件所表示欄位的宣告型別
getInt(Object obj) 同上
getLong(Object obj) 同上
getModifiers()
getName()
getType() 返回一個 Class 物件,它表示了此 Field 物件所表示欄位的宣告型別
set(Object obj,Object value) 將指定物件變數上此 Field 物件表示的欄位設定為指定的新值

空白描述同 Constructor

結束

額,反射好像講完了。。。
用法的例子就不舉了,反正就這麼點東西。
我給大家看兩個我在學習反射過程中記錄的兩個問題吧。

問題一

public class ClassTest {    
    public static void main(String[] args) throws Exception {
        Class humanClass = Human.class();             Human woman = new Woman();
        Method m = humanClass.getDeclaredMethod("test");
        m.setAccessible(true);
        m.invoke(woman);
    }
}

class Human {
    private void test(){
        System.out.println("test()執行");
    }
}

class Woman extends Human {
}複製程式碼

注意:class 用的是Human 類的,暴力反射了Human 類的test 方法,但是我 invoke 傳的物件是一個 Woman 類。
問:m.invoke(women);方法能否正常呼叫 test 方法。

問題二

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

        Class<?> aClass = Class.forName("com.example.admin.materialdesign.test.A$B");
        System.out.println(aClass.getName());
        Object o = aClass.newInstance();
        System.out.println(o.getClass().getName());

    }
}

public class A {
    public class B{
    }
}複製程式碼

已知類 A 和A 的內部類 B,問:main 方法能否正常執行,如果會報錯,會是哪一行程式碼,為什麼?

以上兩個問題是我在反射學習的過程中莫名其妙的遇到的,其中第二個問題跟內部類有關,既然講到這裡,那就順便在這裡把內部類也一塊兒學了吧,反正反射章節的內容也不多,內部類也是個小知識點,我也是在思考第二個問題的過程中學習了內部類。

內部類

Java 執行我們在類的內部定義一個類。如果這個類是沒有 static 修飾,那麼這樣一個巢狀在內部的類成為內部類。內部類被認為是外部物件的一個成員。在定義內部類時,我們同樣有訪問許可權控制。

在使用內部類時,我們要先建立外部物件。由於內部類是外部物件的一個成員,我們可以在物件的內部自由使用內部類,比如:

public class A {
    private int age;

    public void add(){
        B b = new B();
        b.add();
    }

    private class B{

        private void add(){
            age++;
        }

    }
}複製程式碼

上面的例子中,B 為內部類。該內部類有 private 的訪問許可權,因此只能在 Human 內部使用。這樣,B 類就成為一個被 A 專用的內部類。
由於 B 被認為是 A 的一個成員,所以可以互相呼叫。

如果我們修改 B 類的許可權為 public,內部類也能從外部訪問,比如:

A a = new A();
A.B b = a.new B();
b.add();複製程式碼

我們在建立一個內部類物件的時候,必須是基於一個外部類物件,格式如上。這裡的 B 看起來有點像代理模式的感覺,hahah~

看到這裡,我們可以深入理解:內部類物件必須依附與某個外部類物件
與此同時,內部類物件可以訪問它所依附的外部類物件的成員(即使是 private 的成員)。從另一個角度說,內部類物件建立時帶有建立時的環境資訊,這有點像 Python 語言中的閉包。

內部 static 類

我們可以在類的內部定義 static 類,這樣的類稱為巢狀 static 類。

我們可以直接建立巢狀 static 類的物件,而不需要依附於外部類的某個物件。相應的,也無法呼叫物件的非靜態方法,無法修改或讀取外部物件的資料。

好像跟建立一個新的類沒什麼區別。。。如果硬要說有區別,巢狀static 類擴充套件了類的名稱空間。比如 A.B = new A.B();

這裡順便提一下類的載入過程吧,我自己的理解,不一定正確,哈哈哈哈哈~

比如說 new Bean();

1.先在堆記憶體中尋找 Bean 物件的 Class位元組碼(如果有就執行3),如果沒有,則找到。class 檔案,將 class 檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區中的執行時資料結構,在堆中生成一個代表這個類的 Class 物件,作為方法區資料的訪問入口。
2.準備階段:正式為類變數(static)分配並設定類變數初始值的階段,這些記憶體都在方法區中進行分配,執行靜態程式碼塊。
3.初始化:執行類構造方法的過程。注意這個階段父類如果還沒初始化,則先初始化父類。

整個流程是這樣的

  • 如果有父類且未載入,則優先載入父類(同過程1)
  • 優先執行靜態程式碼塊(也就是優先載入位元組碼,父類先執行)
  • 父類的構造方法永遠優先子類執行

問題解答

上面提了兩個比較坑的問題,同學們應該有好好思考吧,如果沒有思考的先思考一會再往下看。

首先,在說我的答案之前,先申明,我的答案未必對,僅僅是我的個人想法,歡迎在評論區拍磚。

1.父類位元組碼暴力獲取父類的 private 方法,然後 invoke 的物件傳了子類的例項

先回顧一下題目的程式碼:

public class ClassTest {    
    public static void main(String[] args) throws Exception {
        Class humanClass = Human.class();             Human woman = new Woman();
        Method m = humanClass.getDeclaredMethod("test");
        m.setAccessible(true);
        m.invoke(woman);
    }
}

class Human {
    private void test(){
        System.out.println("test()執行");
    }
}

class Woman extends Human {
}複製程式碼

首先,我們必須明確一點,父類的 private 方法,子類是無法繼承的。因為子類.getDeclaredMethods();並不能取到父類的 private 方法。

所以?這裡會報錯?

不不不,上面的 test 方法會被正常執行。我們再來看看 Method.invoke()方法中,這個引數的描述。

* @param receiver  the object the underlying method is invoked from複製程式碼

我英語不怎麼好,但是看起來就是要呼叫receiver引數的 Method.getName()這個方法啊。。。。

mmp,不饒路子了,我也沒找到答案,我的猜想是這樣的
Method.invoke(Object receiver);中的 method 是從Human的Class 中取到的,所有在執行的時候大概是把receiver物件強轉成了Human,又因為 Woman 是 Human 的子類,所以強轉不會出錯,所以成功呼叫了 Human 的 test 方法。

2.越過外部類,直接呼叫內部類的構造建立一個內部類物件

我們先回顧一下問題程式碼

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

        Class<?> aClass = Class.forName("com.example.admin.materialdesign.test.A$B");
        System.out.println(aClass.getName());
        Object o = aClass.newInstance();
        System.out.println(o.getClass().getName());

    }
}

public class A {
    public class B{
    }
}複製程式碼

這個問題其實看過我上面關於內部類的介紹並且理解的同學應該已經知道答案了。aClass.newInstance();會報錯。
我們來一步一步分析:
首先我們直接反射內部類 B 的位元組碼,由於 B 是 A 的內部類,所以會先載入 A 類的位元組碼,也就是說 A 的靜態程式碼塊會被呼叫、靜態欄位會被初始化。
然後列印了 aClass 的 name,位元組碼都獲取到了,獲取位元組碼的 name 肯定不會報錯。
然後呼叫了 newInstance 方法建立一個內部類例項,這裡就有問題了,我們剛剛在介紹內部類的時候說過,內部類物件必須依附與某個外部類物件內部類被認為是外部物件的一個成員,所以,直接建立的內部類因為缺少外部環境,必然出錯。

我們再來用反證法證明一下,假如可以建立成功。
現有一個這樣的內部類組合,程式碼如下:

public class A {
    private int age;

    public void add(){
        B b = new B();
        b.add();
    }

    private class B{

        private void add(){
            age++;
        }

    }
}複製程式碼

假如我們直接通過反射建立了內部類 B 的物件 b,那麼呼叫 b.add();方法,方法中的 age 是什麼鬼?所以,內部類物件必須依附與某個外部類物件,不允許被單獨建立。

相關文章