Java核心技術梳理-類載入機制與反射

茶底世界發表於2019-06-19

一、引言

反射機制是一個非常好用的機制,C#和Java中都有反射,反射機制簡單來說就是在程式執行狀態時,對於任意一個類,能夠知道這個類的所有屬性和方法,對於任意一個物件,能夠呼叫它的任意屬性和方法,其實初聽就知道反射是一個比較暴力的機制,它可能會破壞封裝性。

通過反射的定義我們可以想到反射的好處:可以靈活的編寫程式碼,程式碼可以在執行時裝配,降低程式碼的耦合度,動態代理的實現也離不開反射。

為了更好的理解反射,我們先了解下JVM中的類載入機制。

二、類載入機制

當程式要使用某個類時,如果這個類還未載入到記憶體,則需要將其載入到記憶體中,JVM會通過載入、連線、初始化三個步驟來對該類進行初始化。

2.1 類載入

類的載入由類載入器完成,類載入器通常由JVM提供的,JVM提供的類載入器通常稱為系統載入器,開發者可以通過繼承ClassLoader基類來建立自己的類載入器。

類載入載入的是一個二進位制資料,這些資料的來源有幾種:

  • 本地檔案系統載入class檔案。

  • 從JAR包中記載class檔案。

  • 通過網路載入class檔案。

  • 把一個java檔案動態編譯,並執行載入

類載入不一定是要等到首次使用時才載入,虛擬機器允許系統預先載入某些類

2.2 類連線

在類被載入後,系統會為之生成一個對應的Class物件,接著進入連線階段,連線階段是把類的二進位制資料合併到JRE中,類連線分為三個階段:

  1. 驗證:檢驗被載入的類是否有正確的內部結構,並和其他的類協調一致。

  2. 準備:負責為類變數分配記憶體,並設定預設初始化值。

  3. 解析:將類的二進位制資料中的符號引用替換成直接引用。

2.3 類的初始化

虛擬機器負責對類進行初始化,主要是對變數進行初始化,對類變數指定初始值有兩種方式:

  • 宣告類變數時指定初始值。

  • 使用靜態初始化塊為類變數指定初始值。

JVM初始化的幾個步驟:

  1. 假如該類還沒有被載入和連線,則程式先載入或連線該類。

  2. 假如該類的直接父類沒有初始化,則先初始化這該類的父類。

  3. 假如類中有初始化語句,則系統依次執行這些初始化語句。

可以看出當程式主動使用某個類時,一定會保證該類及其所有父類都被初始化。那麼在什麼情況下系統會初始化一個類活著介面呢?

  • 建立類的例項,既包括使用new來建立,也包括通過反射來建立和反序列化來建立。

  • 呼叫某個類的靜態方法。

  • 訪問某個類或介面的類變數。

  • 使用反射方式來強制建立某個類或介面對應的java.jang.class物件。

  • 初始化某個類的子類。

  • 使用java.exe命令來執行某個主類。

三、 類載入器

類載入器是負責將.class檔案載入到記憶體中,並生成對應的java.lang.Class例項,一旦一個類被載入到JVM中,就不會被載入了,這裡就存在一個唯一標識的問題,JVM是通過其全限定類名和其載入器來做唯一標識的,即是通過包名,類名,及載入器名。這也意味著不同類載入器載入的同一個類是不同的。

3.1 類載入機制

當JVM啟動時,會形成三個類載入器組成的初始類載入器層次結構:

  • Bootstrap ClassLoader:根類載入器,負責載入Java的核心類,是由JVM自身提供的。

  • Extension ClassLoader:擴充套件類記載器,負責載入JRE的擴充套件目錄(%JAVA_HOME%/jre/lib/ext)中JAR包中的類。

  • System ClassLoader:系統類載入器,負責在JVM啟動時載入來自Java命令的-classpath選項、java.class.path系統屬性或CLASSPATH環境變數指定的JAR包和類路徑。

JVM的類載入機制主要有三種:

  • 全盤負責:就是當一個類載入器負責載入某個Class時,該class所依賴的和引用的其他Class也將由該類載入器負責載入,除非顯示使用另外一個類載入器載入。

  • 父類委託:先讓父類載入器試圖載入該Class,只有當父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。

  • 快取機制:所有載入過的Class都會被快取,這就是為什麼修改程式碼後必須重新啟動JVM修改才會生效的原因。

類載入器載入Class大致如下:

  1. 檢測此Class是否載入過(通過快取查詢),跳至第8步。

  2. 如果父類載入器不存在,則跳至第4步,如果父類載入器存在,則執行第3步。

  3. 請求使用父類載入器去載入目標類,成功則跳到第8步,否則跳到5步。

  4. 請求使用根類載入器來載入目標類,成功則調到第8步,否則跳至7步。

  5. 當前類載入器嘗試尋找Class檔案,找到執行第6步,找不到則跳入7步。

  6. 從檔案中載入Class,成功跳入第8步。

  7. 丟擲ClassNotFoundException異常。

  8. 返回對應的java.lang.Class物件。

3.2 自定義類載入器

JVM中除了根類載入器外的所有類載入器都是ClassLoader子類的例項,我們可以擴充套件ClassLoader的子類,並重寫其中的方法來實現自定義載入器。ClassLoader有兩個關鍵方法:

  • loadClass(String name, boolean resolve):該方法為ClassLoader的入口點,根據指定名稱來載入類

  • findClass(String name):根據指定名稱來查詢類。

通常推薦findClass(String name),重寫findClass()方法可以避免覆蓋預設類載入器的父類委託和快取機制兩種策略。ClassLoader中還有一個核心方法Class<?> defineClass(String name, byte[] b, int off, int len),該方法負責將指定類的位元組碼檔案讀入到陣列中,並把它轉換成Class物件,不過這個方法是final,不需要我們重寫。

四、反射

前面的通過類載入機制我們知道,每個類被載入後,系統會為該類生成一個對應的Class物件,通過這個物件就可以訪問到JVM中的這個類,在程式中獲取到Class的方式有三種:

  1. 使用Class類中的forName(String name)靜態方法,傳入的引數是某個類的全限定名。

  2. 呼叫某個類的class屬性來獲取該類對應的的Class物件。

  3. 呼叫某個物件的getClass()方法。

在第一個和第二個方法中,都是通過類來獲取到Class物件,但是很明顯第二種更安全也效率更高。

4.1 獲取Class資訊

當我們獲取到Class物件後我們可以根據Class來獲取資訊,Class類提供了大量的方法來獲取Class對應類的詳細資訊,類中主要的資訊包括:建構函式、方法、屬性、註解,另外還有一些基本屬性如:類的修飾符、類名、所在包等。

建構函式

  • Constructor<?>[] getConstructors():返回此Class物件對應類的所有public建構函式。

  • Constructor<?>[] getDeclaredConstructors():返回此Class物件對應類的所有建構函式,無許可權限制

  • Constructor<T> getConstructor(Class<?>... parameterTypes):返回此Class物件對應類的、帶指定形參的列表的public建構函式。

  • Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes):返回此Class物件對應類的、帶指定形參的列表的建構函式,無許可權限制。

方法中存在Declared表示無許可權限制,後面的也與此相同,後面就不列出

方法:

  • Method getMethod(String name, Class<?>... parameterTypes):返回此Class物件對應類的、帶指定形參的列表的public方法。

  • Method[] getMethods():返回此Class物件對應類的所有public方法

成員變數:

  • Field[] getFields():返回此Class物件對應類的所有public成員變數。

  • Field[] getField(String name):返回此Class物件對應類的、指定名稱的public成員變數

註解:

  • Annotation[] getAnnotations():返回修飾該Class物件對應類的所有註解

  • <A extends Annotation> A getAnnotation(Class<A> annotationClass):獲取該Class物件對應類存在的、指定型別的註解,如果不存在,則返回 null。

內部類:

  • Class<?>[] getDeclaredClasses():獲取該Class物件對應類裡包含的全部內部類。

外部類:

  • Class<?> getDeclaringClass():獲取該Class物件對應類裡包含的所在的外部類

介面:

  • Class<?>[] getInterfaces():獲取該Class物件對應類裡所實現的全部介面

基本資訊:

  • int getModifiers():返回此類或介面的所有修飾符,返回的int型別需要解碼。

  • Package getPackage():獲取此類包名。

  • String getName():獲取該Class物件對應類的名稱。

  • String getSimpleName():獲取該Class物件對應類的簡稱。

  • boolean isAnnotation():返回Class物件是否表示一個註解型別。

  • boolean isArray():Class物件是否是一個陣列。

這裡將大體能夠獲取的類資訊列出來了:

public class ClassTest {

    private ClassTest() {
    }

    public ClassTest(String name) {
        System.out.println("有引數的建構函式");
    }

    public void info() {
        System.out.println("無引數的方法");
    }

    public void info(String name) {
        System.out.println("有引數的方法");
    }

    //內部類
    class inner {
    }

    public static void main(String[] args) throws NoSuchMethodException {
        Class<ClassTest> clazz = ClassTest.class;
        Constructor<?>[] constructors = clazz.getConstructors();
        System.out.println("全部public構造器如下:");
        for (Constructor constructor : constructors) {
            System.out.println(constructor);
        }
        Constructor<?>[] pubConstructors = clazz.getDeclaredConstructors();
        System.out.println("全部構造器如下:");
        for (Constructor constructor : pubConstructors) {
            System.out.println(constructor);
        }
        Method[] methods = clazz.getMethods();
        System.out.println("全部public方法如下");
        for (Method method : methods) {
            System.out.println(method);
        }
        System.out.println("名稱為info,並且入參為String型別的方法:" + clazz.getMethod("info", String.class));

    }
}

4.2 生成並操作物件

4.2.1 生成物件

生成物件的方式有兩種,一種是直接呼叫newInstance()方法,這種方式是用預設構造器來建立物件,還有一種方式是先獲得Constructor物件,然後用Constructor呼叫newInstance()來建立物件。

Class<ClassTest> clazz = ClassTest.class;
ClassTest classTest = clazz.newInstance();
classTest.info();
Constructor<ClassTest> constructor = clazz.getConstructor(String.class);
ClassTest class2 = constructor.newInstance("你好");
class2.info();

4.2.2 呼叫方法

上面的例子中,我們可以明確的知道返回的是哪個類,所有呼叫的方法也和之前物件呼叫方法沒有區別,但是一般在用反射機制時,我們是不知道具體類的,這個時候我們可以使用getMethod獲取方法,然後使用invoke來進行方法呼叫:

Class<?> aClass = Class.forName("com.yuanqinnan.api.reflect.ClassTest");
//建立了物件
Object object = aClass.newInstance();
//獲取到方法
Method info = aClass.getMethod("info", String.class);
//呼叫方法
info.invoke(object, "你好");

4.2.3 訪問成員變數

一般情況下,我們會使用getXXX()方法和setXXX(XXX)方法來設定或者獲取成員變數,但是有了反射後,我們可以直接對成員變數進行操作:

public class Person {

    private int age;
    private String name;

    public String toString() {
        return name + ":" + age;
    }

    public static void main(String[] args) throws Exception {
        Person p = new Person();
        Class<Person> personClass = Person.class;
        Field name = personClass.getDeclaredField("name");
        //去掉訪問限制
        name.setAccessible(true);
        name.set(p, "張三");

        Field age = personClass.getDeclaredField("age");
        age.setAccessible(true);
        age.set(p, 20);
        System.out.println(p.toString());
    }
}

從這裡可以看出來,反射破壞了封裝性。

相關文章