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

1、引言

反射機制是一個很是好用的機制,C#和Java中都有反射,反射機制簡單來講就是在程序運行狀態時,對於任意一個類,可以知道這個類的全部屬性和方法,對於任意一個對象,可以調用它的任意屬性和方法,其實初聽就知道反射是一個比較暴力的機制,它可能會破壞封裝性。java

經過反射的定義咱們能夠想到反射的好處:能夠靈活的編寫代碼,代碼能夠在運行時裝配,下降代碼的耦合度,動態代理的實現也離不開反射。api

爲了更好的理解反射,咱們先了解下JVM中的類加載機制。數組

2、類加載機制

當程序要使用某個類時,若是這個類還未加載到內存,則須要將其加載到內存中,JVM會經過加載、鏈接、初始化三個步驟來對該類進行初始化。緩存

2.1 類加載

類的加載由類加載器完成,類加載器一般由JVM提供的,JVM提供的類加載器一般稱爲系統加載器,開發者能夠經過繼承ClassLoader基類來建立本身的類加載器。安全

類加載加載的是一個二進制數據,這些數據的來源有幾種:網絡

  • 本地文件系統加載class文件。函數

  • 從JAR包中記載class文件。spa

  • 經過網絡加載class文件。代理

  • 把一個java文件動態編譯,並執行加載code

類加載不必定是要等到首次使用時才加載,虛擬機容許系統預先加載某些類

2.2 類鏈接

在類被加載後,系統會爲之生成一個對應的Class對象,接着進入鏈接階段,鏈接階段是把類的二進制數據合併到JRE中,類鏈接分爲三個階段:

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

  2. 準備:負責爲類變量分配內存,並設置默認初始化值。

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

2.3 類的初始化

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

  • 聲明類變量時指定初始值。

  • 使用靜態初始化塊爲類變量指定初始值。

JVM初始化的幾個步驟:

  1. 假如該類尚未被加載和鏈接,則程序先加載或鏈接該類。

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

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

能夠看出當程序主動使用某個類時,必定會保證該類及其全部父類都被初始化。那麼在什麼狀況下系統會初始化一個類活着接口呢?

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

  • 調用某個類的靜態方法。

  • 訪問某個類或接口的類變量。

  • 使用反射方式來強制建立某個類或接口對應的java.jang.class對象。

  • 初始化某個類的子類。

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

3、 類加載器

類加載器是負責將.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,不須要咱們重寫。

4、反射

前面的經過類加載機制咱們知道,每一個類被加載後,系統會爲該類生成一個對應的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());
    }
}

從這裏能夠看出來,反射破壞了封裝性。

相關文章
相關標籤/搜索