Java基礎(十八)反射

在學習反射以前,咱們先來學習一些基本知識。java

RTTI

運行時類型識別(RTTI, Run-Time Type Identification)是Java中很是有用的機制,在Java運行時,RTTI維護類的相關信息。api

爲何講這個東西呢,由於咱們今天的主題——反射,也是一種形式的 RTTI。可能有些同窗對 RTTI 有些陌生,其實說白了,就是編譯的時候不知道(或者不須要知道)類的詳細,可是運行的時候須要具體執行代碼。數組

這個概念有點抽象,我給你們舉個例子,好比說咱們的父類引用指向子類對象,而後調用了方法 a(),這個時候是因爲 RTTI 來根據子類是否重寫方法 a()來判斷是執行子類的方法 a(),仍是執行父類的方法 a()。bash

可能有同窗會說上面這不就是動態綁定麼,沒錯,就是動態綁定,可是這也屬於 RTTI 機制。數據結構

再好比,向上轉型、向下轉型、instanceof 這些都屬於RTTI,由於這些都牽涉到運行時類的識別。閉包

而咱們用到的反射也屬於 RTTI 機制,可是和傳統的 RTTI 又有一部分區別。學習

傳統的 RTTI 有3種實現方式spa

  • 向上轉型或向下轉型,在 java 中,向下轉型須要強制類型轉換。
  • CLass 對象(用了 Class 對象,而且只是用 CLass 對象 cast 成指定的類)
  • instanceof

傳統的 RTTI 與反射最主要的區別
最主要的區別在於傳統的 RTTI 在編譯期須要.class文件,而反射不須要。.net

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 是什麼鬼?因此,內部類對象必須依附與某個外部類對象,不容許被單首創建。

相關文章
相關標籤/搜索