「JAVA」Java 字節碼加載,反射機制獲取,詳述動態編程


「JAVA」Java 字節碼加載,反射機制獲取,詳述動態編程

Java 反射

在Java的開發環境中,運行java文件需要使用:java xx.java 命令,運行java命令後,便會啟動JVM,將字節碼文件加載到JVM中,然後開始運行;當運行java命令時,該命令將會啟動一個JVM進程,在這個JVM進程中,會保存有該JVM創建的所有線程、變量、對象,這些線程、變量、對象會共享該JVM的內存區域。

當出現以下情況時,JVM進程會退出:

  1. 程序正常執行結束,JVM正常退出;
  2. 使用System.exit(0)方法,JVM正常退出;
  3. 程序運行出現異常,並且沒有對異常進行處理(捕獲、或者拋出)時;
  4. 操作系統強制結束JVM進程,比如:關機、通過任務管理器強制結束等;

JVM進程一旦退出,該進程中內存區域保存的數據(線程、變量、對象等數據)將會丟失,因為其都是保存在內存中的,並沒有寫入到硬盤。


類加載機制

在Java 類的整個“漫長的”生命週期中,類的加載僅僅只是個開始,因為類在加載後還要經歷一系列的處理,才能被JVM接受,併到處運行。

根據JVM規範,Java 程序的整個生命週期會經歷5個階段:加載 -> 鏈接(驗證、準備、解析) -> 初始化 -> 使用 -> 卸載;因為在鏈接階段會有驗證、準備、解析三個步驟,所以也可以說Java 程序的生命週期會經歷7個階段(使用驗證、準備、解析三個步驟來替代鏈接階段)。


「JAVA」Java 字節碼加載,反射機制獲取,詳述動態編程

Java 程序的生命週期


當JVM要使用某個類某,而該類還未被加載進JVM內存中時,JVM會通過加載,鏈接,初始化三個步驟來對該類進行初始化操作,以便於後期運行。

1.類的加載

類的加載是指將類的class文件(字節碼文件)載入JVM內存中,併為之創建一個java.lang.Class對象,也就是字節碼對象

類的加載過程由類加載器(ClassLoader)完成,類加載器由JVM提供,我們稱之為系統類加載器,同時,我們也可以繼承ClassLoader類來提供自定義類加載器。詳情可查看:

類的加載過程中也會對字節碼文件進行驗證,不過這裡的驗證並不會深入,而是把重點放在字節碼文件的格式上;類加載器不僅可以加載本地字節碼文件,加載jar包中的字節碼,更是可以通過網絡加載字節碼。


2.類的鏈接

當類的字節碼文件被加載進JVM內存之後,JVM便會創建一個對應的Class對象(也可以叫字節碼對象),把字節碼指令中對常量池中的索引引用轉換為直接引用,接著把類的字節碼指令合併到JRE中。鏈接包含三個步驟:

  1. 驗證:檢測被加載的類是否有正確的內部結構。
  2. 準備:負責為類的static變量分配內存,並根據變量的數據類型設置默認值。
  3. 解析:把字節碼指令中對常量池中的索引引用轉換為直接引用。


3.類的初始化

在完成類的鏈接之後,JVM便會對類進行初始化,這裡的初始化主要是對static變量進行初始化,並非是類的實例化,因為類的實例化屬於“使用”階段。

類的初始化一個類包含以下幾個步驟:

  1. 如果該類還未被加載和鏈接,則程序先加載並鏈接該類。
  2. 如果該類的直接父類還未被初始化,則先初始化其父類,若是父類的父類還未初始化,就會先初始化父類的父類,依次類推,直至沒有父類為止。
  3. 如果類中有初始化語句(靜態代碼塊),則JVM會依次執行這些初始化語句。


運行時信息

運行時信息,即Runtime Type Information,是指程序在運行時的信息。在Java 中有兩種方式可以得到運行時信息:

  • 一是通過RTTI,即Run-Time Type Identification,這種方式假設我們在程序編寫時就已經知道了所有對象的類型,主要是通過字節碼對象Class來獲取;
  • 二是通過“反射”機制,反射提供一組api,通過調用api,便能獲取運行時的類信息;Java 中通過java.lang.reflect類庫來支持反射;


Class 對象


Class類:我們平時在開發中定義的類是用來描述業務邏輯的;比如Teacher.java,Student.java等,而Class類用來描述我們所定義的業務邏輯的類,也就是描述類的類。


Class類的實例:其實就是JVM中的字節碼對象,一個字節碼文件有一個字節碼對象,一個Class實例表示在JVM中的某個類或者接口,當然,也包括枚舉和註解,因為枚舉是一種特殊的類,註解是一種特殊的接口。


每一個類都有都有一個Class對象,也就是說每個類都有一個字節碼對象,有一個字節碼文件。當第一次使用類的時候,該類的字節碼文件會被加載到JVM中,創建一個字節碼對象;此時,該字節碼對象就是一個Class實例。

既然每一個類都有一個Class對象,那麼這些Class對象之間是如何區分它所表示的是哪一個類的字節碼的呢?為了解決這個問題,Java 為Class提供了泛型:Class

  • java.lang.String類的字節碼類型:Class<java.lang.string>;/<java.lang.string>
  • java.util.Date類的字節碼類型:Class<java.util.date>;/<java.util.date>
  • java.util.ArrayList類的字節碼類型:Class<java.util.arraylist>;/<java.util.arraylist>


創建Class對象

創建字節碼對象,有3種方式:(使用java.util.Date類做演示)

1.使用類字面量,即使用類的class屬性;

<code>Class<java.util.date> clazz1 = java.util.Date.class;/<java.util.date>/<code>

2.使用對象的getClass();方法;

<code>java.util.Date date = new java.util.Date();
Class> clazz2 = date.getClass();/<code>

3.使用Class.forName(String className);方法;

<code>Class> clazz3 = Class.forName("java.util.Date");/<code>

在上述的代碼案例中使用了通配符“?”,通配符“?”表示“任何類”,通配符的使用使得Class對象的類型更加寬泛。

因為同一個類在JVM中只存在一個字節碼對象,因此在上述的代碼中存在:

<code>clazz1 == cclazz2 == clazz3;/<code>


基本數據類型的字節碼對象

在上述講了三種獲取Class對象的方式,都是類或者對象獲取Class對象;但是基本數據類型不能表示為對象,不能使用getClass的方式;基本數據類型沒有類名,所以也不能使用Class.forName的方式,那麼該如何表示基本類型的字節碼對象呢?

Java 中,所有的數據類型都有class屬性,包括基本數據類型;如何獲取呢?使用字面量的方式,這種方式不僅可以應用於類,還可以應用於接口、數組、和基本數據類型。語法如下:

<code>Class clazz = 數據類型.class;/<code>

詳情如下:

<code>// 基本數據類型字節碼對象
// byte 字節碼對象
byte.class;
// short 字節碼對象
short.class;
// char 字節碼對象
char.class;
// float 字節碼對象
float.class;

// int 字節碼對象
int.class;
// long 字節碼對象
long.class;
// double 字節碼對象
double.class;
// boolean 字節碼對象
boolean.class;
// void 字節碼對象
void.class;/<code>


預加載

在JVM啟動期間,會預先加載一部分核心類庫的字節碼文件到在JVM中,其中就包括了九大基本數據類型。並且在8大基本數據類型的包裝類中,都有一個常量:TYPE,用於返回該包裝類對應基本類的字節碼對象;


「JAVA」Java 字節碼加載,反射機制獲取,詳述動態編程

class & TYPE


那麼如下的代碼案例便解釋得通了,因為Integer和int是不同的數據類型,所以Integer.class == int.class結果為false;

<code>System.out.println(Integer.TYPE == int.class); // true
System.out.println(Integer.class == int.class); // false

// 需要注意:Integer和int是不同的數據類型/<code>


數組的字節碼對象

數組其實是對象,所以數的Class實例便可以使用引用數據類型的方式獲取:

  • 方式1:數組類型.class;
<code>System.out.println(int[].class);/<code>
  • 方式2:數組對象.getClass();
<code>int[] intArr = {1, 2, 3};
System.out.println(intArr.getClass());/<code>

注意:所有的具有相同的維數和相同元素類型的數組共享同一個字節碼對象,和元素沒有關係.

<code>int[] intArr1 = {1, 2, 3, 4, 5};
int[] intArr2 = {1, 2, 3};

System.out.println(intArr1.getClass()); // class [I
System.out.println(intArr2.getClass()); // class [I

// intArr1 和 intArr2共享一同一個字節碼對象,跟數組的元素沒有關係/<code>


Class類和Object類

  • Object:描述所有的對象,其擁有所有對象的共同的方法。
  • Class:描述所有的類型,其擁有所有類型的相同的方法;需要注意:Class類也是Object的子類

  • 每個類都有其字節碼對象,通過字節碼對象能獲取到類的很多信息,在Class類中提供了很多的api來獲取類的運行時信息,以下是一些常用的:

    <code>// forName() 這是一組靜態方法,用於獲取指定類的字節碼對象 

    static Class> forName(String className); // className 類的全限定名稱
    // className 類的全限定名稱,initialize 是否初始化,loader 指定的類加載器
    static Class>\tforName(String name, boolean initialize, ClassLoader loader)

    // 獲取類的全限定名
    String getName();

    // 獲取類的簡單名稱,比如:Integer,String等
    String getSimpleName();

    // 獲取類的完整名稱
    String getCanonicalName();

    // 獲取當前類的所實現的所有接口
    Class>[] getInterfaces();

    // 獲取當前類的父類
    Class super T>\tgetSuperclass();

    // 判斷是否是數組
    boolean\tisArray();
    // 判斷是否是接口
    boolean\tisInterface()

    /<code>


    但是,上述的通過字節碼對象獲取類的信息是建立在我們提前知道確切的數據類型的基礎之上的,如果我們事先不知道數據類型,那麼便不能通過獲取類的字節碼對象來獲取類的運行時信息;在這樣的情況下,又該如何獲取對象的運行時信息,該如何動態的創建類的對象?

    對於這個問題,java.lang.reflect類庫中提供了一個叫做“反射”的組件,可以配合Class類來共同提供動態信息獲取,下面,就來一起看看吧!


    反射


    「JAVA」Java 字節碼加載,反射機制獲取,詳述動態編程

    何為“反射”


    眾所知周,對象有編譯類型和運行類型。現有如下的代碼案例:

    <code>Object    obj  =  new java.util.Date();

    // 編譯類型: Object
    // 運行類型: java.util.Date/<code>


    需求:通過obj對象,調用java.util.Date類中的toLocaleString方法:obj.toLocaleString(); ,此時編譯報錯,如何解決編譯錯誤?


    案例分析:編譯時,會檢查該編譯類型中是否存在toLocaleString方法;如果存在,編譯成功,否則編譯失敗;此時編譯失敗是因為在Object類中沒有toLocaleString方法,因此要將obj轉換為java.util.Date類型,才能正確調用toLocaleString方法。


    解決方案:因為obj的真實類型是java.util.Date類,所以我們可以把obj對象強制轉換為java.util.Date類型:

    <code>java.util.Date d = (java.util.Date)obj;
    d.toLocaleString();/<code>


    上述解決方案是在知道obj真實類型的情況下解決的,那麼如果不知道obj的真實類型,就不能進行強轉,也不能使用Class對象;此時,問題又該如何解決?


    遇到此等問題,就不得不祭出法寶——反射了,在java.lang.reflect類庫中提供了一組api用於提供對反射的支持。


    反射:獲取類的元數據的過程,在運行期間,動態的獲取類中的成員信息,包括構造器、方法、字段、內部類、父類、接口等;並且把類中的每一種成員,都分別描述成一個新的類:

    • Class:表示所有的類;
    • Constructor:表示所有的構造器;
    • Method:表示所有的方法;
    • Field:表示所有的字段;

    元數據:描述類的類的數據。

    在Java 中,Class類和java.lang.reflect類庫共同提供了對“反射”的支持,在java.lang.reflect類庫中就包含了Constructor、Method、Field等類,用於描述類的元數據,在字節碼對象中提供了獲取這些元數據的api:

    <code>// 獲取所有字段
    Field[] getFields();
    // 獲取所有構造器
    Constructor>[] getConstructors();
    // 後去所有方法
    Method[]\tgetMethods();/<code>


    反射之構造器

    獲取構造器

    在Class類中提供了獲取類的構造器的方法:

    <code>// 該方法只能獲取當前Class所表示類的public修飾的構造器
    public Constructor>[] getConstructors();

    // 獲取當前Class所表示類的所有的構造器,和訪問權限無關,可用於獲取私有的構造器
    public Constructor>[] getDeclaredConstructors();

    // 獲取當前Class所表示類中指定的一個public的構造器
    public Constructor getConstructor(Class>... parameterTypes);/<code>

    在上述方法中:

    • Constructor類表示類中構造器的類型,Constructor的實例就是某個類中的某一個構造器;
    • 參數 parameterTypes表示:構造器參數的Class類型,方法可藉由傳入的參數類型來獲取對應的構造器;

    Constructor類提供了用於通過反射獲取到的構造器的來創建實例的方法:newInstance();

    <code>// 獲取構造器public Student(String name) {}
    import java.lang.reflect.*;

    public class ReflectDemo {

    \tpublic static void main(String[] args) throws Exception {
    \t\t// 獲取Studentr類的字節碼對象
    \t\tClass<student> clazz = Student.class;

    \t\t// 獲取Student類的構造器:public Student(String name) {}
    \t\tConstructor<student> userConstructor = clazz.getConstructor(String.class);
    \t\tSystem.out.println("userConstructor:" + userConstructor);

    // 通過獲取的公共構造器來創建實例
    Student student = userConstructor.newInstance("老夫不正經");
    \t}

    }
    /<student>/<student>/<code>


    由於上述方法時在Class類中,所有在調用這些方法前需要先獲取類的字節碼對象,再通過字節碼對象來調用獲取構造器的方法。

    先有Student類,在Student類中有三個構造器,分別是:無參構造器,公共帶參構造器,私有構造器;代碼如下:

    <code>


    無參構造器

    <code>// 獲取構造器public Student(String name) {}
    import java.lang.reflect.*;

    public class ReflectDemo {

    \tpublic static void main(String[] args) throws Exception {
    \t\t// 獲取Studentr類的字節碼對象
    \t\tClass<student> clazz = Student.class;
    \t\t// 獲取Student類的構造器:public Student(String name) {}

    \t\tConstructor<student> userConstructor = clazz.getConstructor(String.class);
    \t\tSystem.out.println("userConstructor:" + userConstructor);

    // 通過獲取的公共構造器來創建實例
    Student student = userConstructor.newInstance("老夫不正經");
    \t}

    }
    /<student>/<student>/<code>


    公共構造器

    <code>// 獲取構造器private Student(String name, Integer id) {}
    import java.lang.reflect.*;

    public class ReflectDemo {

    \tpublic static void main(String[] args) throws Exception {
    \t\t// 獲取Studentr類的字節碼對象
    \t\tClass<student> clazz = Student.class;
    \t\t// 獲取Student類的構造器:private Student(String name, Integer id) {}
    \t\tConstructor<student> userConstructor = clazz.getDeclaredConstructor(String.class, Integer.class);
    \t\tSystem.out.println("userConstructor:" + userConstructor);

    // 通過獲取的私有構造器來創建實例
    // 設置構造器可以訪問
    userConstructor.setAccessible(true);
    Student student = userConstructor.newInstance("老夫不正經", 1);
    \t}

    }
    /<student>/<student>/<code>


    私有構造器

    :在使用私有構造器創建實例時,由於私有構造器不能被外界訪問,所以在創建實例前先設置其可以訪問,案例代碼如下:

    <code>// 獲取構造器private Student(String name, Integer id) {}
    import java.lang.reflect.*;

    public class ReflectDemo {

    \tpublic static void main(String[] args) throws Exception {
    \t\t// 獲取Studentr類的字節碼對象
    \t\tClass<student> clazz = Student.class;
    \t\t// 獲取Student類的構造器:private Student(String name, Integer id) {}
    \t\tConstructor<student> userConstructor = clazz.getDeclaredConstructor(String.class, Integer.class);
    \t\tSystem.out.println("userConstructor:" + userConstructor);

    // 通過獲取的私有構造器來創建實例
    // 設置構造器可以訪問
    userConstructor.setAccessible(true);
    Student student = userConstructor.newInstance("老夫不正經", 1);
    \t}

    }
    /<student>/<student>/<code>


    如果一個類中的構造器是外界可以直接訪問(非private 的),同時沒有參數,那麼可以直接使用Class類中的newInstance方法創建對象示例,其效果等同於使用new關鍵創建對象。


    反射之方法

    獲取方法

    在Class類中提供了獲取類的方法的api,由於方法也是在Class類中,所以在獲取方法之前,也是需要先獲取其所在類的字節碼對象,再來獲取方法,最後再來執行方法。

    Class類中獲取類的方法的api:

    <code>// 獲取包括自身和繼承過來的所有的public方法
    public Method[] getMethods();

    // 獲取自身類中所有的方法(不包括繼承的,和訪問權限無關)
    public Method[] getDeclaredMethods();

    // 表示調用指定的一個公共的方法(包括繼承的)
    public Method getMethod(String methodName, Class>... parameterTypes);
    // methodName:表示被調用方法的名字
    // parameterTypes:表示被調用方法的參數的Class類型,如String.class

    // 表示調用指定的一個本類中的方法(不包括繼承的)
    public Method getDeclaredMethod(String methodName, Class>... parameterTypes);
    // methodName:表示被調用方法的名字
    // parameterTypes:表示被調用方法的參數的Class類型,如String.class/<code>

    上述這些api的返回值均為Method類,Method類是用於描述反射中的方法的類,在Method類中,提供了執行反射獲取的方法的方法invoke():

    <code>// 表示調用當前Method所表示的方法
    public Object invoke(Object obj, Object... args);
    // obj: 表示被調用方法底層所屬對象
    // args:表示調用方法是傳遞的實際參數
    \t\t\t// 方法返回值Object為:底層方法的執行結果/<code>


    下面,通過一個案例來實踐上述方法的使用:

    同樣,會有Student類,類中有public方法,static方法、private方法,分別用於演示通過反射獲取類中public方法,static方法,private方法;

    Student類:

    <code>public class Student {

    \tpublic void publicMethod(String name) {
    \tSystem.out.println("public method " + name);
    }

    public static void staticMethod(String name) {
    \tSystem.out.println("static method " + name);
    }

    private void privateMethod(String name) {
    \tSystem.out.println("private method " + name);
    }

    }
    /<code>


    獲取所有方法

    <code>// 表示調用當前Method所表示的方法
    public Object invoke(Object obj, Object... args);
    // obj: 表示被調用方法底層所屬對象
    // args:表示調用方法是傳遞的實際參數
    \t\t\t// 方法返回值Object為:底層方法的執行結果/<code>


    調用public方法

    <code>import java.lang.reflect.*;

    public class ReflectMethodDemo {

    \t\tpublic static void main(String[] args) {

    \t\tClass<student> clazz = Student.class;
    \t\t// 獲取public 方法
    \t\tMethod publicMethod = clazz.getMethod("publicMethod", String.class);
    \t\t// 執行方法
    \t\t\tObject result = publicMethod.invoke(calzz.newInstance(), "老夫不正經");
    }

    }/<student>/<code>


    調用static方法,使用反射調用靜態方法時,由於靜態方法不屬於任何對象,它只屬於類本身。所以在執行靜態方法時,傳入invoke方法的第一個參數就不能是任何對象,在這裡需要將其設置為null。

    <code>import java.lang.reflect.*;

    public class ReflectMethodDemo {

    \t\tpublic static void main(String[] args) {

    \t\tClass<student> clazz = Student.class; \t\t
    \t\t// 獲取static 方法
    \t\tMethod staticMethod = clazz.getMethod("staticMethod", String.class);
    \t\t// 執行方法,這裡將對象設置為null
    \t\t\tObject result = staticMethod.invoke(null, "老夫不正經");
    }

    }/<student>/<code>


    調用private方法,在調用私有方法之前,要設置該方法為可訪問的,因為Method是AccessibleObject子類,所以在Method對象中可以通過調用setAccessible(true);來設置訪問可以訪問。

    <code>import java.lang.reflect.*;

    public class ReflectMethodDemo {

    \t\tpublic static void main(String[] args) {

    \t\tClass<student> clazz = Student.class;

    \t\t// 獲取private 方法
    \t\tMethod publicMethod = clazz.getMethod("privateMethod", String.name);
    \t\t// 設置方法為可訪問的
    \t\tstaticMethod.setAccessible(true);
    // 執行方法
    \t\t\tObject result = staticMethod.invoke(calzz.newInstance(), "老夫不正經");
    }

    }/<student>/<code>


    可變參數,方法的可變參數在底層是其實是作為數組處理的,所以在執行可變參數的方法時,可直接傳入數組參數,但傳參數,須得區分引用數據類型和基本數據類型;

    <code>import java.lang.reflect.*;

    public class StaticMethodDemo {
    \t\t\t\t// 可變參數是引用類型
    \t\t\t\tpublic static void doSomething(Stirng... args) {
    \t\tSystem.out.println("靜態方法的可變參數,參數是引用類型:" + args);
    }

    // 可變參數是基本數據類型
    \t\t\t\tpublic static void doAnything(int... args) {
    \t\tSystem.out.println("靜態方法的可變參數,參數是基本數據類型:" + args);
    }

    public static void main(String[] args) {
    Class<staticmethoddemo> clazz = StaticMethodDemo.class;
    // 獲取 引用類型參數的方法
    Method method1 = clazz.getMethod("doSomething", String[].class);
    // 執行方法
    Object result = method1.invoke(null, new Object[] {new String[]{"lao", "fu"}}); // 正確
    \t\t\t\t\t\t\t\t// Object result = method1.invoke(null, new String[]{"lao", "fu"}); // 錯誤
    \t\t\t\t\t\t\t\t// Object result = method1.invoke(null, "lao", "fu"); // 錯誤

    \t\t\t\t\t\t\t\t// 獲取 引用類型參數的方法
    Method method1 = clazz.getMethod("doAnything", int[].class);
    // 執行方法
    Object result = method1.invoke(null, new Object[] {new int[]{13, 14}}); // 正確

    \t\t\t\t\t\t\t\tObject result = method1.invoke(null, new int[]{13, 14}); // 正確
    \t\t\t\t\t\t\t\t// Object result = method1.invoke(null, 13, 14); // 錯誤
    }
    }/<staticmethoddemo>/<code>

    在本文中,介紹了類的加載過程,詳細描述的了JVM對字節碼文件的處理過程,從中也看到了不少JVM底層的處理細節。

    字節碼對象的創建使得RTTI得以獲取大量被底層屏蔽的信息,通過這些信息能讓我們更加了解Java 的設計思想。

    通過反射提供的強大功能,不僅可以訪問到類的運行時信息,還可以訪問到類中不允許被外界訪問的private屬性和方法,這無異於又打開了另一個新世界的大門,我們能通過反射編寫動態代碼,使得編程更加靈活。這也是Java 語言的一大特色,能夠很好的與C、C++這樣的語言區分開來。

    到這裡就結束了,能完整看完的小夥伴,已實屬不易,給你們點贊!由於作者知識有限,若有錯誤之處,還請多多指出!

    完結。老夫雖不正經,但老夫一身的才華


    分享到:


    相關文章: