天天看點

「Java基礎」什麼?反射居然這麼簡單!

本章将會深入介紹 Java類的加載、連接配接和初始化知識,并重點介紹 java.lang.reflect 包下的接口和類,包括 Class、Method、Field、Constructor 和 Array等,這些類分别代表類、方法、成員變量、構造器和數組,Java 程式可以使用這些類動态地擷取某個對象。某個類的運作時資訊,并可以動态地建立 Java 對象,動态地調用Java方法,通路并修改指定對象的成員變量值。

類的加載

當程式主動使用某個類時,如果該類還未被加載到記憶體中,則系統會通過加載、連接配接、初始化三個步驟來對該類進行初始化。如果沒有意外,JVM 将會連續完成這三個步驟,是以有時也把這三個步驟統稱為類加載或類初始化。類加載指的是将類的 class 檔案讀入記憶體,并為之建立一個 java.lang.Class 對象,也就是說,當程式中使用任何類時,系統都會為之建立一個 java.lang.Class 對象。

類的加載由類加載器完成,類加載器通常由 JVM提供,這些類加載器也是前面所有程式運作的基礎,JVM 提供的這些類加載器通常被稱為系統類加載器。除此之外,開發者可以通過繼承 ClassLoader基類來建立自己的類加載器。

通過使用不同的類加載器,可以從不同來源加載類的二進制資料,通常有如下幾種來源。

  • 從本地檔案系統加載 class 檔案,這是前面絕大部分示例程式的類加載方式。
  • 從JAR 包加載 class 檔案,這種方式也是很常見的,前面介紹 JDBC 程式設計時用到的資料庫驅動類就放在 JAR 檔案中,JVM 可以從 JAR 檔案中直接加載該 class 檔案。
  • 通過網絡加載class 檔案。
  • 把一個 Java 源檔案動态編譯,并執行加載。

Class檔案結構

Class檔案是一組以8位位元組為基礎機關的二進制流,各個資料項目嚴格按照順序緊湊地排列在Class檔案之中,中間沒有添加任何分隔符,這使得整個Class檔案中存儲的内容幾乎全部是程式運作的必要資料,沒有空隙存在。當遇到需要占用8位位元組以上空間的資料項時,則會按照高位在前的方式分割成若幹個8位位元組進行存儲。一個Class檔案包含一下幾部分:

  1. 魔數:每個Class檔案的頭4個位元組稱為魔數(Magic Number),它的唯一作用是确定這個檔案是否為一個能被虛拟機接受的Class檔案。很多檔案存儲标準中都使用魔數來進行身份識别,譬如圖檔格式,如gif或者jpeg等在檔案頭中都存有魔數。使用魔數而不是擴充名來進行識别主要是基于安全方面的考慮,因為檔案擴充名可以随意地改動。檔案格式的制定者可以自由地選擇魔數值,隻要這個魔數值還沒有被廣泛采用過同時又不會引起混淆即可。
  2. 版本号:緊接着魔數的4個位元組存儲的是Class檔案的版本号:第5和第6個位元組是次版本号(Minor Version),第7和第8個位元組是主版本号(Major Version)。Java的版本号是從45開始的,JDK 1.1之後的每個JDK大版本釋出主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下相容以前版本的Class檔案,但不能運作以後版本的Class檔案,即使檔案格式并未發生任何變化,虛拟機也必須拒絕執行超過其版本号的Class檔案。
  3. 常量池:緊接着主次版本号之後的是常量池入口,常量池可以了解為Class檔案之中的資源倉庫,它是Class檔案結構中與其他項目關聯最多的資料類型,也是占用Class檔案空間最大的資料項目之一,常量池中主要存放兩大類常量:字面量(Literal)和符号引用(SymbolicReferences)。字面量比較接近于Java語言層面的常量概念,如文本字元串、聲明為final的常量值等。而符号引用則屬于編譯原理方面的概念,包括了下面三類常量:
    1. 類和接口的全限定名
    2. 字段的名稱和描述符
    3. 方法的名稱和描述符
  1. 通路标志:在常量池結束之後,緊接着的兩個位元組代表通路标志(access_fags),這個标志用于識别一些類或者接口層次的通路資訊,包括:這個Class是類還是接口;是否定義為public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final等。
  2. 類索引、父類索引、與接口索引集合:Class檔案中由這三項資料來确定這個類的繼承關系。類索引用于确定這個類的全限定名,父類索引用于确定這個類的父類的全限定名。由于Java語言不允許多重繼承,是以父類索引隻有一個,除了java.lang.Object之外,所有的Java類都有父類,是以除了java.lang.Object外,所有Java類的父類索引都不為0。接口索引集合就用來描述這個類實作了哪些接口,這些被實作的接口将按implements語句(如果這個類本身是一個接口,則應當是extends語句)後的接口順序從左到右排列在接口索引集合中。
  3. 字段表集合:字段表(field_info)用于描述接口或者類中聲明的變量。字段(field)包括類級變量以及執行個體級變量,但不包括在方法内部聲明的局部變量。描述執行個體變量和類變量以及執行個體變量的資訊包括:字段的作用域(public、private、protected修飾符)、是執行個體變量還是類變量(static修飾符)、可變性(final)、并發可見性(volatile修飾符,是否強制從主記憶體讀寫)、可否被序列化(transient修飾符)、字段資料類型(基本類型、對象、數組)、字段名稱。
  4. 方法表集合:Class檔案存儲格式中對方法的描述與對字段的描述幾乎采用了完全一緻的方式,方法表的結構如同字段表一樣,依次包括了通路标志、名稱索引、描述符索引、屬性表集合幾項。
  5. 屬性表集合:在Class檔案、字段表、方法表都可以攜帶自己的屬性表集合,以用于描述某些場景專有的資訊。與Class檔案中其他的資料項目要求嚴格的順序、長度和内容不同,屬性表集合的限制稍微寬松了一些,不再要求各個屬性表具有嚴格順序,并且隻要不與已有屬性名重複,任何人實作的編譯器都可以向屬性表中寫入自己定義的屬性資訊,Java虛拟機運作時會忽略掉它不認識的屬性。

類加載器

類加載器負責加載所有的類,系統為所有被載入記憶體中的類生成一個java.lang.Class執行個體。一旦一個類被載入JVM中,同一個類就不會被再次載入了。在Java中,一個類用其全限定類名(包括包名和類名)作為辨別;但在JVM中,一個類用其全限定類名和其類加載器作為其唯一辨別。

當JVM啟動時,會形成由3個類加載器組成的初始類加載器層次結構。

  • Bootstrap ClassLoader:根類加載器。它負責加載Java的核心類。
  • Extension ClassLoader:擴充類加載器。它負責加載 JRE 的擴充目錄(%JAVA_HOME%/jre/lib/ext)中JAR包的類。
  • System ClassLoader:系統類加載器。它負責在JVM啟動時加載來自java指令的-classpath選項、java.class.path系統屬性,或CLASSPATH環境變量所指定的JAR包和類路徑。

類加載機制

JVM的類加載機制主要有如下3種。

  • 全盤負責,所謂全盤負責,就是當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他 Class 也将由該類加載器負責載入,除非顯式使用另外一個類加載器來載入。
  • 雙親委派,所謂父類委托,則是先讓 parent(父)類加載器試圖加載該Class,隻有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。
  • 緩存機制。緩存機制将會保證所有加載過的 Class 都會被緩存,當程式中需要使用某個 Class 時,類加載器先從緩存區中搜尋該Class,隻有當緩存區中不存在該Class對象時,系統才會讀取該類對應的二進制資料,并将其轉換成Class對象,存入緩存區中。

類加載過程

類從被加載到虛拟機記憶體中開始,到解除安裝出記憶體為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中驗證、準備、解析3個部分統稱為連接配接(Linking),這7個階段的發生順序如下圖所示。

「Java基礎」什麼?反射居然這麼簡單!

下面将詳細講解在類加載過程中每一步中的詳細過程:

1.加載

“加載”是“類加載”(Class Loading)過程的一個階段,希望讀者沒有混淆這兩個看起來很相似的名詞。在加載階段,虛拟機需要完成以下3件事情:

  1. 通過一個類的全限定名來擷取定義此類的二進制位元組流。
  2. 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
  3. 在記憶體中生成一個代表這個類的java.lang.Class對象,作為方法區這個類的各種資料的通路入口。

2.驗證

驗證是連接配接階段的第一步,這一階段的目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。

  1. 檔案格式驗證,第一階段要驗證位元組流是否符合Class檔案格式的規範,并且能被目前版本的虛拟機處理。這一階段可能包括下面這些驗證點:
    1. 是否以魔術0xCAFEBABE
    2. 主、次版本号是否在目前虛拟機處理範圍内
    3. 常量池的常量中是否有不被支援的類型
    4. 指向常量的各種索引值是否有執行不存在的常量或者不符合類型的常量
    5. CONSTANT_Utf8_info型的常量中是否有不符合UTF8編碼的資料。
    6. Class檔案中各個部分及檔案本身是否有被删除的或附加的其他資訊。
  1. 中繼資料驗證,第二階段是對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求,這個階段可能包括的驗證點如下:
    1. 這個類是否有父類(除了java.lang.Object之外,所有的類都應當有父類)。
    2. 這個類的父類是否繼承了不允許被繼承的類(被final修飾的類)。
    3. 如果這個類不是抽象類,是否實作了其父類或接口之中要求實作的所有方法。
    4. 類中的字段、方法是否與父類産生沖突(例如覆寫了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一緻,但傳回值類型卻不同等)。
  1. 位元組碼驗證,第三階段是整個驗證過程中最複雜的一個階段,主要目的是通過資料流和控制流分析,确定程式語義是合法的、符合邏輯的。在第二階段對中繼資料資訊中的資料類型做完校驗後,這個階段将對類的方法體進行校驗分析,保證被校驗類的方法在運作時不會做出危害虛拟機安全的事件,例如:
    1. 保證任意時刻操作數棧的資料類型與指令代碼序列都能配合工作,例如不會出現類似
    2. 這樣的情況:在操作棧放置了一個int類型的資料,使用時卻按long類型來加載入本地變量表中。
    3. 保證跳轉指令不會跳轉到方法體以外的位元組碼指令上。
    4. 保證方法體中的類型轉換是有效的,例如可以把一個子類對象指派給父類資料類型,這是安全的,但是把父類對象指派給子類資料類型,甚至把對象指派給與它毫無繼承關系、完全不相幹的一個資料類型,則是危險和不合法的。
  1. 符号引用驗證,最後一個階段的校驗發生在虛拟機将符号引用轉化為直接引用的時候,這個轉化動作将在連接配接的第三階段——解析階段中發生,符号引用驗證可以看做是對類自身以外(常量池中的各種符号引用)的資訊進行比對性校驗,通常需要校驗下列内容:
    1. 符号引用中通過字元串描述的全限定名是否能找到對應的類。
    2. 在指定類中是否存在符合方法的字段描述符以及簡單名稱所描述的方法和字段。
    3. 符号引用中的類、字段、方法的通路性(private、protected、public、default)是否可被目前類通路。

3.準備

準備階段是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些變量所使用的記憶體都将在方法區中進行配置設定。這個階段中有兩個容易産生混淆的概念需要強調一下,首先,這時候進行記憶體配置設定的僅包括類變量(被static修飾的變量),而不包括執行個體變量,執行個體變量将會在對象執行個體化時随着對象一起配置設定在Java堆中。

4.解析

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程,符号引用是以一組符号來描述所引用的目标,符号可以是任何形式的字面量,隻要使用時能無歧義地定位到目标即可。符号引用與虛拟機實作的記憶體布局無關,引用的目标并不一定已經加載到記憶體中。各種虛拟機實作的記憶體布局可以各不相同,但是它們能接受的符号引用必須都是一緻的,因為符号引用的字面量形式明确定義在Java虛拟機規範的Class檔案格式中。直接引用則是指:可以是直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用是和虛拟機實作的記憶體布局相關的,同一個符号引用在不同虛拟機執行個體上翻譯出來的直接引用一般不會相同。如果有了直接引用,那引用的目标必定已經在記憶體中存在。

5.初始化

類初始化階段是類加載過程的最後一步,前面的類加載過程中,除了在加載階段使用者應用程式可以通過自定義類加載器參與之外,其餘動作完全由虛拟機主導和控制。到了初始化階段,才真正開始執行類中定義的Java程式代碼(或者說是位元組碼)。在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程式員通過程式制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

通過反射擷取類資訊

擷取Class對象

每個類被加載之後,系統就會為該類生成一個對應的Class對象,通過該Class對象就可以通路到JVM中的這個類。在Java程式中獲得Class對象通常有如下3種方式。

  1. 使用Class類的forName(String clazzName)靜态方法。該方法需要傳入字元串參數,該字元串參數的值是某個類的全限定類名(必須添加完整包名)。
  2. 調用某個類的class屬性來擷取該類對應的Class對象。例如,Person.class将會傳回Person類對應的Class對象。
  3. 調用某個對象的getClass()方法。該方法是java.lang.Object類中的一個方法,是以所有的Java對象都可以調用該方法,該方法将會傳回該對象所屬類對應的Class對象。

下面,通過示例來示範擷取Class對象

package cn.bytecollege;
public class ClassTest {
	public static void main(String[] args) throws ClassNotFoundException {
		//第一種方式:通過forName("");
		Class c1 = Class.forName("java.lang.String");
		//第二種方式:通過類名
		Class c2 = String.class;
		//第三種方式:getClass();
		String s = new String();
		Class c3 = s.getClass();
	}
}           

擷取構造器

Class類提供了大量的執行個體方法來擷取該Class對象所對應類的詳細資訊,下面4個方法用于擷取Class對應類所包含的構造器。

下面4個方法用于擷取Class對應類所包含的構造器。

  • Connstructor getConstructor(Class<?>... parameterTypes):傳回此Class對象對應類的指定public構造器。
  • Constructor<?>[]getConstructors():傳回此Class對象對應類的所有public構造器。
  • Constructor<T> getDeclaredConstructor(Class<?>...parameterTypes):傳回此Class對象對應類的指定構造器,與構造器的通路權限無關。
  • Constructor<?>[]getDeclaredConstructors():傳回此Class對象對應類的所有構造器,與構造器的通路權限無關。

下面通過示例示範擷取構造器:

package cn.bytecollege;

public class Person {
    private String name;
    private String gender;
    private Integer age;

    public Person(){
        System.out.println("無參構造");
    }

    public Person(String name,String gender,Integer age){
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
}           
package cn.bytecollege;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException {
        Class clazz = Class.forName("cn.bytecollege.Person");

        Constructor con1 = clazz.getConstructor();

        Constructor con2 = clazz.getConstructor();
    }
}           

通過反射可以擷取類中定義的構造方法,擷取了構造方法就可以利用反射擷取的構造方法來建立對象。

通過反射來生成對象有如下兩種方式。

  • 使用Class對象的newInstance()方法來建立該Class對象對應類的執行個體,這種方式要求該 Class 對象的對應類有預設構造器,而執行 newInstance()方法時實際上是利用預設構造器來建立該類的執行個體。
  • 先使用 Class 對象擷取指定的 Constructor 對象,再調用 Constructor 對象的newInstance()方法來建立該Class對象對應類的執行個體。通過這種方式可以選擇使用指定的構造器來建立執行個體。

下面通過示例來示範利用反射建立對象:

package cn.bytecollege;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Test {
    public static void main(String[] args) throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            InstantiationException, IllegalAccessException {
        //擷取class對象
        Class clazz = Class.forName("cn.bytecollege.Person");

        //擷取無參構造方法
        Constructor con1 = clazz.getConstructor();
        //通過class建立對象
        Persion p1 = (Persion) con1.newInstance();

        //擷取帶參構造方法
        Constructor con2 = clazz.getConstructor(String.class,String.class,Integer.class);
        //通過class建立對象
        Person p2 = (Person) con2.newInstance("張三","男",18);

    }
}           

擷取類包含的方法

下面4個方法用于擷取Class對應類所包含的方法。

  • Method getMethod(String name,Class<?>...parameterTypes):傳回此Class對象對應類的指定public方法。
  • Method[]getMethods():傳回此Class對象所表示的類的所有public方法。
  • Method getDeclaredMethod(String name,Class<?>...parameterTypes):傳回此Class對象對應類的指定方法,與方法的通路權限無關。
  • Method[] getDeclaredMethods():傳回此Class對象對應類的全部方法,與方法的通路權限無關。
package cn.bytecollege;

import java.util.Date;

public class Teacher {
	String name;
    Integer age;
    Date birthday;


    public void method1(String str){
        System.out.println("public 修飾的方法");
    }
    private void method2(){
        System.out.println("private 修飾的方法");
    }

    String method3(String name, Integer sex, Date age){
        System.out.println("預設修飾"+name+" "+sex+" "+age);
        return name+" "+sex+" "+age;
    }

    protected void method4(){
        System.out.println("protected 修飾的方法");
    }
}           
package cn.bytecollege;

import java.lang.reflect.Method;
import java.util.Date;

public class Test {
	public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException {

        //加載Class對象
        //會報出不存在該類的異常
        Class c=Class.forName("cn.bytecollege.Teacher");

        //擷取所有公共方法
        System.out.println("================擷取所有公共方法=================");
        Method[] methods=c.getMethods();
        for (Method method:methods) {
            System.out.println("公共方法:"+method);
        }
        //擷取所有方法
        System.out.println("================擷取所有的方法=================");
        Method[] declaredMethods=c.getDeclaredMethods();
        for (Method declaredmethod:declaredMethods) {
            System.out.println("所有方法:"+declaredmethod);
        }

        System.out.println("================擷取特定(帶參)方法=================");
        Method method1=c.getMethod("method1",String.class);
        System.out.println("特定(帶參)方法:"+method1);

        System.out.println("================擷取特定(不帶參)方法=================");
        Method method2=c.getDeclaredMethod("method2");
        System.out.println("特定(不帶參)方法:"+method2);

        System.out.println("================擷取特定(多參)方法=================");
        Method method3=c.getDeclaredMethod("method3", String.class, Integer.class, Date.class);
        System.out.println("特定(多參)方法:"+method3);
    }
}           

運作後的結果是:

「Java基礎」什麼?反射居然這麼簡單!

每個Method對象對應一個方法,獲得Method對象後,程式就可通過該Method來調用它對應的方法。在Method裡包含一個invoke()方法,該方法的簽名如下。

  • Object invoke(Object obj,Object...args):該方法中的obj是執行該方法的主調,後面的args是執行該方法時傳入該方法的實參。
package cn.bytecollege;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Msym {
    public void test(String[] arg){
        for (String string : arg) {
            System.out.println(string);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException,
    NoSuchMethodException, InvocationTargetException, 
    InstantiationException, IllegalAccessException {
        //擷取位元組碼對象
        Class clazz = Class.forName("cn.bytecollege.Msym");
        //擷取一個對象
        Constructor con =  clazz.getConstructor();
        Msym m = (Msym) con.newInstance();
        String[] s = new String[]{"aa","bb"};
        //擷取Method對象
        Method method = clazz.getMethod("test", String[].class);
        //調用invoke方法來調用
        method.invoke(m, s);
    }
}           

擷取類包含的Field

  • Field getField(String name):傳回此Class對象對應類的指定public Field。
  • Field[]getFields():傳回此Class對象對應類的所有public Field。
  • Field getDeclaredField(String name):傳回此Class對象對應類的指定Field,與Field的通路權限無關。
  • Field[] getDeclaredFields():傳回此Class對象對應類的全部 Field,與Field的通路權限無關。
package cn.bytecollege;

public class Student {
    String name;

    public Student(String name){
        this.name = name;
    }
}
           
package cn.bytecollege;

import java.lang.reflect.Field;

public class Test3 {
    public static void main(String[] args) throws NoSuchFieldException,
    SecurityException,IllegalArgumentException,IllegalAccessException {

        Student a = new Student("Byte");

        Class cl = a.getClass();
        Field field = cl.getDeclaredField("name");
        field.setAccessible(true);
        Object s = field.get(a);
        System.out.println(s);
        field.set(a, "byte");
        s = field.get(a);
        System.out.println(s);
    }
}           

通過Class對象的getFields()或getField()方法可以擷取該類所包括的全部Field或指定Field。Field提供了如下兩組方法來讀取或設定Field值。

  • getXxx(Object obj):擷取obj對象該Field的屬性值。此處的Xxx對應8個基本類型,如果該屬性的類型是引用類型,則取消get後面的Xxx。
  • setXxx(Object obj,Xxx val):将obj對象的該Field設定成val值。此處的Xxx對應8個基本類型,如果該屬性的類型是引用類型,則取消set後面的Xxx。

反射操作數組

在java.lang.reflect包下還提供了一個Array類,Array對象可以代表所有的數組。程式可以通過使用Array來動态地建立數組,操作數組元素等。

Array提供了如下幾類方法。

  • static Object newInstance(Class<?>componentType,int...length):建立一個具有指定的元素類型、指定次元的新數組。
  • static xxx getXxx(Object array,int index):傳回array數組中第index個元素。其中xxx是各種基本資料類型,如果數組元素是引用類型,則該方法變為get(Object array,int index)。
  • static void setXxx(Object array,int index,xxx val):将array數組中第index個元素的值設為val。其中xxx是各種基本資料類型,如果數組元素是引用類型,則該方法變成set(Object array,int index,Object val)。

下面程式示範了如何使用Array來生成數組,為指定數組元素指派,并擷取指定數組元素的方式:

package cn.bytecollege;

import java.lang.reflect.Array;

public class ArrayTest {
    public static void main(String[] args) {
        try{
            //建立一個元素類型為String,長度為10的數組
            Object arr = Array.newInstance(String.class,10);
            //依次為arr數組中index為5、6的元素指派
            Array.set(arr,5,"Byte");
            Array.set(arr,6,"科技");
            //依次取出arr數組中index為5、6的元素的值;
            Object str1 = Array.get(arr,5);
            Object str2 = Array.get(arr,6);
            //輸出arr數組中index為5、6的元素
            System.out.print(str1);
            System.out.print(str2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}