天天看點

Java 類加載機制與反射

Java 類加載機制與反射

1.1 JVM 和類

當調用 java 指令運作某個 Java 程式時,該指令将會啟動一個 Java 虛拟機程序,不管 Java 程式有多麼複雜,該程式啟動了多少個線程,它們都處于該 Java 虛拟機程序裡。

同一個 JVM 的所有線程、所有變量都處于同一個程序裡,它們都使用該 JVM 程序的記憶體區域。

當系統出現以下幾種情況時, JVM 程序将被終止

    1. 程式運作到最後正常結束

    2. 程式運作到使用 System.exit() 或 Runtime.getRuntime().exit()代碼處結束程式

    3. 程式在執行過程中遇到未捕獲的異常或錯誤而結束

    4. 程式所在的平台強制結束了 JVM 程序

1.2 類的加載

------------------------------------------------------

當程式主動使用某個類時,如果該類還沒有被加載到記憶體中,則系統會通過加載、連接配接、初始化三個步驟對該類進行初始化,如果沒有意外, JVM 将會連續完成這三個步驟,是以有時也把這三個步驟統稱為類加載或初始化

類加載指的是将類的 class 檔案讀入記憶體,并為之建立一個 java.lang.Class 對象,也就是說,當程式中使用任何類時,系統都會為之建立一個 java.lang.Class 對象。

每個類是一批具有相同特征的對象的抽象,而系統中所有的類實際上也是執行個體,它們都是 java.lang.Class 的執行個體。

類的加載由類加載器完成,類加載器通常由 JVM 提供,這些類加載器也是所有程式運作的基礎, JVM 提供的這些類加載器通常被稱為系統類加載器。

除此之外,開發者可以通過繼承 ClassLoader 基類建立自己的類加載器。

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

    1. 從本地檔案系統加載 class 檔案

    2. 從 JAR 包加載 class  檔案

    3. 通過網絡加載 class 檔案

    4. 把一個 Java 源檔案動态編譯,并執行加載

類加載器通常無須等到 “首次使用” 該類時才加載該類, Java 虛拟機規範允許系統預先加載某些類。

1.3 類的連接配接

----------------------------------------------------

當類被加載之後,系統為之生成一個對應的 Class 對象,接着将會進入連接配接階段,連接配接階段負責把類的二進制資料合并到 JRE 中。

類連接配接又可以分為如下三個階段:

    1. 驗證:驗證階段用于檢驗被加載的類是否有正确的内部結構,并和其他類協調一緻

    2. 準備:類準備階段則負責為類變量配置設定記憶體,并設定預設初始值

    3. 解析:将類的二進制資料的符号引用替換成直接引用

1.4 類的初始化

-----------------------------------------------------

在類的初始化階段,虛拟機負責對類進行初始化,主要就是對類變量進行初始化。

在 Java 類中對類變量指定初始值有兩種方式:①聲明類變量時指定初始值;②使用靜态初始化塊為類變量指定初始值

public class Test

{

    // 聲明變量 a 時指定初始值

    static int a = 5;

    static int b;

    static int c;

    static

    {

        // 使用靜态初始化塊為類變量 b 指定初始值

        b = 6;

    }

}

聲明變量時指定初始值,靜态初始化塊都将被當成類的初始化語句, JVM 會按這些語句在程式中的排列順序一次執行它們。

JVM 初始化一個類包含如下幾個步驟:

    1. 假如這個類還沒有被加載和連接配接,則程式先加載并連接配接該類

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

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

JVM 最先初始化的總是 java.lang.Object 類。當程式主動使用任何一個類時,系統會保證該類以及所有父類(包括直接父類和間接父類)都會初始化

1.5 類初始化的時機

------------------------------------------------------------------

當 Java 程式首次通過下面6種方式使用某個類或接口時,系統就會初始化該類或接口:

    1. 建立類的執行個體:為某個類建立執行個體的方式包括:使用 new 操作符來建立執行個體,通過放射來建立執行個體,通過反序列化的方式建立執行個體

    2. 調用某個類的方法(靜态方法)

    3. 通路某個類或接口的類變量,或為該類變量指派

    4. 使用反射方式來強制建立某個類或接口對應的 java.lang.Class 對象。例如, Class.forName("Person"), 如果系統還未初始化 Person 類,則這行代碼将會導緻該 Person 類被初始化,并傳回 Person 類對應的 java.lang.Class 對象

    5. 初始化某個類的子類。當初始化某個類的子類時,該類的所有父類都會被初始化

    6. 直接使用 java.exe 指令來運作某個主類,程式會先初始化該主類

除此之外,下面幾種情況需要特别指出。

    1.  對于 final 型的類變量,如果該類變量的值在編譯時就可以确定下來,那麼這個類變量相當于宏變量。 Java 編譯器會在編譯時直接把這個類變量出現的地方替換成它的值,是以即使程式使用該靜态變量,也不會導緻該類的初始化。

        當某個類變量(也叫靜态變量)使用了 final 修飾符,而且它的值可以在編譯時就确定下來,那麼程式其他地方使用該變量時,實際上并沒有使用該類變量,而是相當于使用常量。

        反之,如果 final 修飾的類變量的值不能在編譯時确定下來,則必須等到運作時才可以确定該類變量的值,如果通過該類來通路它的類變量,則會導緻該類被初始化。

    2. 當使用 ClassLoader 類的 loadClass() 方法來加載某個類時,該方法隻是加載該類,并不會執行該類的初始化。 使用 Class 的 forName() 靜态方法才會導緻強制初始化該類。

*

*

*

2 類加載器

-----------------------------------------------------------------------------------------------------------

類加載器負責将 .class 檔案(可能在磁盤上,也可能在網絡上)加載到記憶體中,并為之生成對應的 java.lang.Class 對象。一旦一個類被載入 JVM 中,統一各類就不會被再次載入。

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

    1. Bootstrap ClassLoader: 根類加載器

    2. Extension ClassLoader: 擴充類加載器

    3. System ClassLoader: 系統類加載器

    Bootstrap ClassLoader 被稱為引導(也稱為原始或根)類加載器,它負責加載 java 的核心類。

    在 sun 的 JVM 中,當執行 java.exe 指令時,使用 -Xbootclasspath 選項或 -D 選項指定 sun.boot.class.path 系統屬性值可以指定加載附加的類。

    根類加載器非常特殊,它并不是 java.lang.ClassLoader 的子類,而是 JVM 自身實作的。

    下面的程式可以獲得根類加載器所加載的核心類庫

public class BootstrapTest

{

    public static void main(String[] args)

    {

        // 擷取根類加載器所加載的全部URL數組

        URL[] urls = sun.misc.Launcher.

        getBootstrapClassPath().getURLs();

        // 周遊、輸出根類加載器加載的全部URL

        for (int i = 0; i < urls.length; i++)

        {

            System.out.println(urls[i].toExternalForm());

        }

    }

}

    Extension ClassLoader 被稱為擴充類加載器,它負責加載 JRE 的擴充目錄 (%JAVA_HOME%/jre/lib/ext 或者由 java.ext.dirs 系統屬性指定的目錄) 中的 JAR 包的類

    System ClassLoader 被稱為系統 (也稱為應用)類加載器,它負責在 JVM 啟動時加載來自 Java 指令的 -classpath 選項、 java.class.path 系統屬性, 或 CLASSPATH 環境變量所指定的 JAR 包和類路徑。

    程式可以通過 ClassLoader 的靜态方法 getSystemClassLoader() 擷取系統類加載器。如果沒有特别指定,則使用者自定義的類加載器都以此類加載器作為父加載器。

2.2 類加載機制

-----------------------------------------------

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

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

    2. 父類委托:所謂父類委托,則是先讓父類加載器試圖加載該 Class,隻有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類。

    3. 緩存機制:緩存機制将會保證所有加載過的 Class 都會緩存,當程式中需要使用某個 Class 時,類加載器先從緩存區中搜尋該 Class,隻有當緩存區中不存在該 Class 對象時,系統才會讀取該類對應的二進制資料,并将其轉換成 Class 對象,存入緩存區。

    除了可以使用 Java 提供的類加載器之外,開發者也可以實作自己的類加載器,自定義的類加載器通過繼承 ClassLoader 來實作。

    類加載器之間的父子關系并不是繼承上的父子關系,這裡的父子關系是類加載器執行個體之間的關系。

    JVM 中這4種類加載器的層次結構圖:

                |-------------------|

                |    根類加載器        |

                |--------|----------|    

                        /|\

                         |

                |--------|----------|

                |    擴充類加載器    |

                |--------|----------|    

                        /|\

                         |

                |--------|----------|

                    系統類加載器

                |--------|----------|

                        /|\

                         |

                |--------|----------|

                |    使用者類加載器    |

                |-------------------|

                JVM 中的4種類加載器

public class ClassLoaderPropTest

{

    public static void main(String[] args)

        throws IOException

    {

        // 擷取系統類加載器

        ClassLoader systemLoader = ClassLoader.getSystemClassLoader();

        System.out.println("系統類加載器:" + systemLoader);

        Enumeration<URL> em1 = systemLoader.getResources("");

        while(em1.hasMoreElements())

        {

            System.out.println(em1.nextElement());

        }

        // 擷取系統類加載器的父類加載器:得到擴充類加載器

        ClassLoader extensionLader = systemLoader.getParent();

        System.out.println("擴充類加載器:" + extensionLader);

        System.out.println("擴充類加載器的加載路徑:"

            + System.getProperty("java.ext.dirs"));

        System.out.println("擴充類加載器的parent: "

            + extensionLader.getParent());

    }

}

從運作結果看出,系統類加載器是 AppClassLoader 的執行個體,擴充類加載器是 ExtClassLoader 的執行個體。實際上,這兩個類都是 URLClassLoader 類的執行個體。

JVM 的根類加載器并不是 Java 實作的,而且由于程式通常無須通路根類加載器,是以通路擴充類加載器的父類加載器時傳回 null。

類加載器加載 Class 大緻要經過如下 8 個步驟:

    1. 檢測此 Class 是否載入過 (即在緩存區中是否有此 Class), 如果有則直接進入第 8 步,否則接着執行第2步

    2. 如果父類加載器不存在(如果沒有父類加載器,則要麼 parent 一定是根加載器,要麼本身就是根加載器),則跳到第4步,如果父類加載器存在,則接着執行第3步。

    3. 請求使用父類加載器去載入目标類,如果成功載入則跳到第8步,否則接着執行第5步

    4. 請求使用根類加載器來載入目标類,如果成功載入則跳到第8步,否則跳到第7步

    5. 目前類加載器嘗試尋找 Class 檔案(從此 ClassLoader 相關路徑中尋找),如果找到則執行第6步,如果找不到則跳到第7步。

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

    7. 抛出 ClassNotFoundException 異常

    8. 傳回對應的 java.lang.Class 對象。

    其中,第5、6步允許重寫 ClassLoader 的 findClass() 方法來實作自己的載入政策,甚至重寫 loadClass() 方法來實作自己的載入過程。

2.3 建立并使用自定義的類加載器

---------------------------------------------------------------------

ClassLoader 類有如下兩個關鍵方法:

    protected Class<?>     loadClass(String name, boolean resolve) : 該方法為 ClassLoader 的入口點,根據指定名稱加載類, 系統就是調用該方法來擷取指定類對應的 Class 對象。

    protected Class<?>     findClass(String name) : 根據指定名稱查找類

    如果需要實作自定義 ClassLoader, 可以通過重寫以上連個方法實作,通常推薦重寫 findClass() 方法。

    loadClass() 方法的執行步驟:

        1. 調用 findLoadedClass(String) 檢查是否已經加載類,如果已經加載則直接傳回

        2. 在父類加載器上調用 loadClass() 方法,如果父類加載器為 null, 則使用根類加載器來加載

        3. 調用 findClass(String) 方法查找類。

    ClassLoader 裡還有一個核心方法: protected Class<?>     defineClass(String name, byte[] b, int off, int len), 該方法負責将指定類的位元組碼檔案讀入位元組數組 byte[] b 内,并把它轉換為 Class 對象。該位元組碼檔案可以來源于檔案、網絡等。該方法為 final , 無須重寫。

2.4 URLClassLoader 類

---------------------------------------------------------------------

Java 為 ClassLoader 提供了一個 URLClassLoader 實作類,該類是系統類加載器和擴充類加載器的父類(此處的父類指的是類與類之間的繼承關系)。

URLClassLoader 功能比較強大,它既可以從本地檔案系統擷取二進制檔案來加載類,也可以從遠端主機擷取二進制檔案來加載類,程式可以直接使用 URLClassLoader 加載類。

構造器:

    URLClassLoader(URL[] urls)

    URLClassLoader(URL[] urls, ClassLoader parent)

    URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)

一旦得到 URLClassLoader 對象之後,就可以調用該對象的 loadClass() 方法來加載指定的類。

例子:

public class URLClassLoaderTest

{

    private static Connection conn;

    // 定義一個擷取資料庫連接配接方法

    public static Connection getConn(String url ,

        String user , String pass) throws Exception

    {

        if (conn == null)

        {

            // 建立一個URL數組

            URL[] urls = {new URL(

                "file:mysql-connector-java-5.1.30-bin.jar")};

            // 以預設的ClassLoader作為父ClassLoader,建立URLClassLoader

            URLClassLoader myClassLoader = new URLClassLoader(urls);

            // 加載MySQL的JDBC驅動,并建立預設執行個體

            Driver driver = (Driver)myClassLoader.

                loadClass("com.mysql.jdbc.Driver").newInstance();

            // 建立一個設定JDBC連接配接屬性的Properties對象

            Properties props = new Properties();

            // 至少需要為該對象傳入user和password兩個屬性

            props.setProperty("user" , user);

            props.setProperty("password" , pass);

            // 調用Driver對象的connect方法來取得資料庫連接配接

            conn = driver.connect(url , props);

        }

        return conn;

    }

    public static void main(String[] args)throws Exception

    {

        System.out.println(getConn("jdbc:mysql://localhost:3306/mysql"

            , "root" , "32147"));

    }

}

*

*

*

3 通過反射檢視類的資訊

---------------------------------------------------------------------------------------------------------------

Java 程式中的許多對象在運作時會出現兩種類型:編譯時類型和運作時類型

Person p = new Student();

這行代碼将會生成一個 p 變量,該變量的編譯時類型是 Person, 運作時類型為 Student;

有更極端的情形,程式在運作時接收到外部傳入的一個對象,該對象的編譯時類型是 Object, 但程式又需要調用該對象運作時類型的方法。

為了解決這些問題,程式需要在運作時發現對象和類的真實資訊。解決該問題有以下兩種做法:

    1. 第一種做法是假設在編譯時和運作時都完全知道類型的具體資訊,這種情況下,可以先用 instanceof 運算符進行判斷,再利用強制類型轉換将其轉換成其運作時類型變量。

    2. 第二種做法是編譯時根本無法預知該對象和類可能屬于哪些類,程式隻能依靠運作時資訊來發現該對象和類的真實資訊,這就必須使用反射。

3.1 擷取 Class 對象

----------------------------------------------------------------

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

    1. 使用 Class 類的 forName(String clazzName) 靜态方法。該字元串參數的值是某個類的全限定名(添加完整的包名)。

    2. 調用某個類的 class 屬性來擷取該類對應的 Class 對象,例如 Person.class 将會傳回 Person 類對應的 Class 對象

    3. 調用某個對象的 getClass() 方法,該方法是 java.lang.Object 類的一個方法,是以所有的 Java 對象都可以調用該方法,該方法傳回該對象所屬類對應的 Class 對象

對于第一種方式和第二種方式都可以直接根據類來取得該類的 Class 對象,相比之下,第二種方式有兩個優勢

        1. 代碼更安全,程式在編譯階段就可以檢查需要通路的 Class 對象是否存在

        2. 程式性能更好

        是以,大部分情況下都應該使用第二種方式來擷取類的 Class 對象

獲得某個類的 Class 對象後,程式就可以調用 Class 對象的方法來擷取該對象和類的真實資訊

3.2 從 Class 中擷取資訊

-----------------------------------------------------------------

Class 類提供了大量的執行個體方法來擷取該 Class 對象所對應類的詳細資訊:

     Constructor<T>     getConstructor(Class<?>... parameterTypes);

     Constructor<?>[]     getConstructors();

     Constructor<T>     getDeclaredConstructor(Class<?>... parameterTypes);

     Constructor<?>[]     getDeclaredConstructors();

     Method     getMethod(String name, Class<?>... parameterTypes);

     Method[]     getMethods();

     Method     getDeclaredMethod(String name, Class<?>... parameterTypes);

     Method[]     getDeclaredMethods();

     Field     getField(String name);

     Field[]     getFields();

     Field     getDeclaredField(String name);

     Field[]     getDeclaredFields();

3.3 Java 8 新增的方法參數反射

------------------------------------------------------------------

Java 8 java.lang.reflect 包下新增了一個 Executable 抽象基類,該對象代表可執行的類成員,該類派生了 Constructor、 Method 兩個子類

Parameter 也是 Java 8 新增的 API。

需要指出的是,使用 javac 指令編譯 Java 源檔案時,預設生成的 class 檔案并不包含方法的形參名資訊,是以調用 isNamePresent() 方法将傳回 false, 調用 getName() 方法也不能得到該參數的形參名。

如果希望javac 指令編譯 Java 源檔案時保留形參資訊,則需要為該指令指定 -parameters 選項。

class Test

{

    public void replace(String str, List<String> list){}

}

public class MethodParameterTest

{

    public static void main(String[] args)throws Exception

    {

        // 擷取String的類

        Class<Test> clazz = Test.class;

        // 擷取String類的帶兩個參數的replace()方法

        Method replace = clazz.getMethod("replace"

            , String.class, List.class);

        // 擷取指定方法的參數個數

        System.out.println("replace方法參數個數:" + replace.getParameterCount());

        // 擷取replace的所有參數資訊

        Parameter[] parameters = replace.getParameters();

        int index = 1;

        // 周遊所有參數

        for (Parameter p : parameters)

        {

            if (p.isNamePresent())

            {

                System.out.println("---第" + index++ + "個參數資訊---");

                System.out.println("參數名:" + p.getName());

                System.out.println("形參類型:" + p.getType());

                System.out.println("泛型類型:" + p.getParameterizedType());

            }

        }

    }

}

執行:javac -parameters MethodParameterTest.java

*

*

*

4 使用反射生成并操作對象

--------------------------------------------------------------------------------------------------------

Class 對象可以獲得該類裡的方法(由 Method 對象表示)、構造器(由 Constructor 對象表示)、成員變量(由 Field 對象表示)。

程式可以通過 Method 對象來執行對應的方法,通過 Constructor 對象來調用對應的構造器建立執行個體,能通過 Field 對象直接通路并修改對象的成員變量值。

4.1 建立對象

---------------------------------------------------------------

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

    1. 使用 Class 對象的 newInstance() 方法建立該 Class 對象對應的類的執行個體,這種方式要求該 Class 對象的對應類有預設構造器,而執行 newInstance() 方法時實際上是利用預設構造器來建立該類的執行個體。

    2. 先用 Class 對象擷取指定的 Constructor 對象,再調用 Constructor 對象的 newInstance() 方法來建立該 Class 對象對應類的執行個體。通過這種方式可以選擇使用指定的構造器來建立執行個體。

通過第一種方式來建立對象比較常見,很多 Java EE 架構都需要根據配置檔案資訊來建立 Java 對象,從配置檔案讀取的隻是某個類的字元串的類名,程式需要根據該字元串來建立對應的執行個體,就必須用到反射。

例子:

public class ObjectPoolFactory

{

    // 定義一個對象池,前面是對象名,後面是實際對象

    private Map<String ,Object> objectPool = new HashMap<>();

    // 定義一個建立對象的方法,

    // 該方法隻要傳入一個字元串類名,程式可以根據該類名生成Java對象

    private Object createObject(String clazzName)

        throws InstantiationException

        , IllegalAccessException , ClassNotFoundException

    {

        // 根據字元串來擷取對應的Class對象

        Class<?> clazz = Class.forName(clazzName);

        // 使用clazz對應類的預設構造器建立執行個體

        return clazz.newInstance();

    }

    // 該方法根據指定檔案來初始化對象池,

    // 它會根據配置檔案來建立對象

    public void initPool(String fileName)

        throws InstantiationException

        , IllegalAccessException ,ClassNotFoundException

    {

        try(

            FileInputStream fis = new FileInputStream(fileName))

        {

            Properties props = new Properties();

            props.load(fis);

            for (String name : props.stringPropertyNames())

            {

                // 每取出一對key-value對,就根據value建立一個對象

                // 調用createObject()建立對象,并将對象添加到對象池中

                objectPool.put(name ,

                    createObject(props.getProperty(name)));

            }

        }

        catch (IOException ex)

        {

            System.out.println("讀取" + fileName + "異常");

        }

    }

    public Object getObject(String name)

    {

        // 從objectPool中取出指定name對應的對象。

        return objectPool.get(name);

    }

    public static void main(String[] args)

        throws Exception

    {

        ObjectPoolFactory pf = new ObjectPoolFactory();

        pf.initPool("obj.txt");

        System.out.println(pf.getObject("a"));      // ①

        System.out.println(pf.getObject("b"));      // ②

    }

}

如果不想利用預設構造器建立 Java 對象,而想用指定的構造器來建立對象,則需要利用 Constructor 對象,每個 Constructor 對應一個構造器,需要如下三個步驟:

    1. 擷取該類的 Class 對象

    2. 利用 Class 對象的 getConstructor() 擷取指定的構造器

    3. 調用 Constructor 的 newInstance() 方法建立 Java 對象

例子:

public class CreateJFrame

{

    public static void main(String[] args)

        throws Exception

    {

        // 擷取JFrame對應的Class對象

        Class<?> jframeClazz = Class.forName("javax.swing.JFrame");

        // 擷取JFrame中帶一個字元串參數的構造器

        Constructor ctor = jframeClazz

            .getConstructor(String.class);

        // 調用Constructor的newInstance方法建立對象

        Object obj = ctor.newInstance("測試視窗");

        // 輸出JFrame對象

        System.out.println(obj);

    }

}

如果要唯一地确定某個類中的構造器,隻要指定構造器的形參清單即可。

當調用 Constructor 對象的 newInstance() 方法時通常需要傳入參數,因為調用 Constructor 的 newInstance() 方法實際上等于調用它對應的構造器,傳給 newInstance() 方法的參數将作為構造器的參數。

對于上面 CreateJFrame 中已知 java.swing.JFrame 類的情形,通常沒有必要使用反射來建立該對象,畢竟通過反射建立對象時性能要稍低些,

實際上,隻有當程式需要動态建立某個類的對象時才會考慮使用反射,通常在開發通用性比較廣的架構、基礎平台時可能會大量使用反射。

4.2 調用方法

------------------------------------------------------------------------------

當獲得某個類對應的 Class 對象後,就可以通過該 Class 對象的 getMethods() 方法或者 getMethod() 方法擷取全部方法或指定方法。

每個 Method 對象對應一個方法,獲得 Method 對象後,程式就可以通過該 Method() 來調用它對應的方法。

Method 對象包含一個 invoke() 方法,用于調用 Method 對象對應的方法。

    Object     invoke(Object obj, Object... args);

方法中 obj 是執行該方法的主體對象,後面的 args 是執行該方法時傳入該方法的實參

當通過 Method 的 invoke() 方法調用對應的方法時, Java 會要求程式必須有調用該方法的權限,如果程式确實需要調用某個對象的 private 方法,則可以先調用    

void setAccessible(boolean flag);

将 Method 對象的 accessible 設定為指定的布爾值,值為 true, 訓示該 Method 在使用時應該取消 Java 語言的通路權限檢查,值為 false,則訓示 Method 在使用時要實施 Java 語言的通路權限檢查

實際上, setAccessible() 方法并不屬于 Method, 而是屬于它的父類 AccessibleObject 。是以 Method Constructor Field 都可以調用該方法,進而實作通過反射來調用 private 方法、private 構造器、private 成員變量。

它們通過該方法取消通路權限的檢查,通過反射即可通路 private 成員。

4.3 通路成員變量的值

---------------------------------------------------------------------------------    

通過 Class 對象的 getFields() 或 getField() 方法可以擷取該類所包含的全部成員變量或指定成員變量。

Field 提供了如下兩組方法讀取或設定成員變量的值

    1. getXxx(Object obj): 擷取 obj 對象的該成員變量的值

    2. setXxx(Object obj, Xxx val): 将 obj 對象的該成員變量設定成 val 值

例子:

class Person

{

    private String name;

    private int age;

    public String toString()

    {

        return "Person[name:" + name +

        " , age:" + age + " ]";

    }

}

public class FieldTest

{

    public static void main(String[] args)

        throws Exception

    {

        // 建立一個Person對象

        Person p = new Person();

        // 擷取Person類對應的Class對象

        Class<Person> personClazz = Person.class;

        // 擷取Person的名為name的成員變量

        // 使用getDeclaredField()方法表明可擷取各種通路控制符的成員變量

        Field nameField = personClazz.getDeclaredField("name");

        // 設定通過反射通路該成員變量時取消通路權限檢查

        nameField.setAccessible(true);

        // 調用set()方法為p對象的name成員變量設定值

        nameField.set(p , "Yeeku.H.Lee");

        // 擷取Person類名為age的成員變量

        Field ageField = personClazz.getDeclaredField("age");

        // 設定通過反射通路該成員變量時取消通路權限檢查

        ageField.setAccessible(true);

        // 調用setInt()方法為p對象的age成員變量設定值

        ageField.setInt(p , 30);

        System.out.println(p);

    }

}

4.4 操作數組

-------------------------------------------------------------------------------------------------

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

Array 提供三類靜态方法:    

    static Object     newInstance(Class<?> componentType, int length)

    static Object     newInstance(Class<?> componentType, int... dimensions)

    static Object     get(Object array, int index)

    static void     set(Object array, int index, Object value)

例子:

public class ArrayTest1

{

    public static void main(String args[])

    {

        try

        {

            // 建立一個元素類型為String ,長度為10的數組

            Object arr = Array.newInstance(String.class, 10);

            // 依次為arr數組中index為5、6的元素指派

            Array.set(arr, 5, "瘋狂Java講義");

            Array.set(arr, 6, "輕量級Java EE企業應用實戰");

            // 依次取出arr數組中index為5、6的元素的值

            Object book1 = Array.get(arr , 5);

            Object book2 = Array.get(arr , 6);

            // 輸出arr數組中index為5、6的元素

            System.out.println(book1);

            System.out.println(book2);

        }

        catch (Throwable e)

        {

            System.err.println(e);

        }

    }

}

*

*

*

5 使用反射生成 JDK 動态代理

---------------------------------------------------------------------------------------------------------------------------

代理(Proxy) 在《現代漢語詞典》裡的解釋是:受當事人委托,代表他進行某種活動。

應用到面向對象領域,應該是:代理對象代表另一個對象(目标對象, Target ),執行相關活動,也就是說,在實際應用中使用代理對象作為中介,代替目标對象。

在 Java 中,代理對象往往實作和目标對象一緻的接口,并作為目标對象的代替,接收對象使用者( Client )的調用,并将全部或部分調用轉發給目标對象。

在這個過程中,實作代理接口和調用轉發,是代理對象必須完成的兩個重要任務,缺一不可。

在完成上述兩個任務的同時,代理對象可以在調用轉發前或者調用轉發後執行一些功能,這些功能可以簡單,如輸出日志,實施通路控制等;也可以非常複雜,包括通路資料庫,或者加載遠端資源等。

也正是這些在調用前後執行的附加邏輯,展現了代理對象的價值。

Java 的 java.lang.reflect 包提供了一個 Proxy 類和一個 Interface InvocationHandler, 通過使用這個類和接口可以生成 JDK 動态代理類或動态代理對象。

5.1 使用 Proxy 和 InvocationHandler 建立動态代理

---------------------------------------------------------

Proxy 提供了用于建立動态代理類和代理對象的靜态方法,它也是所有動态代理類的父類。

如果在程式中為一個或多個接口動态地生成實作類,就可以使用 Proxy 來建立動态代理類;如果需要為一個或多個接口動态地建立執行個體,也可以使用 Proxy 來建立動态代理執行個體。

Proxy 提供如下兩個方法來建立動态代理類和動态代理執行個體:

    static Class<?>     getProxyClass(ClassLoader loader, Class<?>... interfaces):建立一個動态代理類所對應的 Class 對象,該代理類将實作 interfaces 所指定的所有接口。

    static Object         newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h):直接建立一個動态代理對象,該代理對象的實作類實作了 interfaces 指定的一系列接口,執行代理對象的每個方法時都會被替換執行 InvocationHandler 對象的 invoke() 方法

當程式使用反射方式為指定接口生成系列動态代理對象時,這些動态代理對象的實作類實作了一個或多個接口。動态代理對象就需要實作一個或多個接口裡定義的所有方法,

但問題是:系統怎麼知道如何實作這些方法? 這個時候就輪到 InvocationHandler 對象登場了------當執行動态代理對象裡的方法時,實際上會替換成調用 InvocationHandler 對象的 invoke() 方法。

程式中可以采用先生成一個動态代理類,然後通過動态代理類建立代理對象的方式生成動态代理對象:

    //To create a proxy for some interface Foo:

    //建立一個 InvocationHandler 對象

    InvocationHandler handler = new MyInvocationHandler(...);

    //使用 Proxy 生成動态代理類 proxyClass

    Class<?> proxyClass = Proxy.getProxyClass(Foo.class.getClassLoader(), Foo.class);

    //擷取 proxyClass 類中帶有一個 InvocationHandler 參數的構造器,調用構造器的 newInstance() 方法建立動态執行個體

    Foo f = (Foo) proxyClass.getConstructor(InvocationHandler.class).

                     newInstance(handler);

    ----------------------------------------------------------------------------------

    上面的代碼也可簡化成如下代碼:

    //建立一個 InvocationHandler 對象

    InvocationHandler handler = new MyInvocationHandler(...);

    //使用 Proxy 直接生成一個動态代理對象

    Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),

                                          new Class<?>[] { Foo.class },

                                          handler);

    ----------------------------------------------------------------------------------

例子:

interface Person

{

    void walk();

    void sayHello(String name);

}

class MyInvokationHandler implements InvocationHandler

{

    public Object invoke(Object proxy, Method method, Object[] args)

    {

        System.out.println("----正在執行的方法:" + method);

        if (args != null)

        {

            System.out.println("下面是執行該方法時傳入的實參為:");

            for (Object val : args)

            {

                System.out.println(val);

            }

        }

        else

        {

            System.out.println("調用該方法沒有實參!");

        }

        return null;

    }

}

public class ProxyTest

{

    public static void main(String[] args)

        throws Exception

    {

        // 建立一個InvocationHandler對象

        InvocationHandler handler = new MyInvokationHandler();

        // 使用指定的InvocationHandler來生成一個動态代理對象

        Person p = (Person)Proxy.newProxyInstance(Person.class.getClassLoader()

            , new Class[]{Person.class}, handler);

        // 調用動态代理對象的walk()和sayHello()方法

        p.walk();

        p.sayHello("孫悟空");

    }

}

public interface InvocationHandler 接口: 定義該實作類需要實作 invoke() 方法, 調用代理對象是所有方法都會被替換成調用 invoke() 方法。

    Object invoke(Object proxy, //代表動态代理對象

              Method method,    //代表正在執行的方法

              Object[] args)    //代表調用目标方法時傳入的實參。

       throws Throwable

    Returns:

    the value to return from the method invocation on the proxy instance. If the declared return type of the interface method is a primitive type,

    then the value returned by this method must be an instance of the corresponding primitive wrapper class; otherwise, it must be a type assignable to the declared return type.

    If the value returned by this method is null and the interface method's return type is primitive, then a NullPointerException will be thrown by the method invocation on the proxy instance.

    If the value returned by this method is otherwise not compatible with the interface method's declared return type as described above, a ClassCastException will be thrown by the method invocation on the proxy instance.

JDK 動态代理隻能為接口建立動态代理    

實際上,在普通程式設計過程中,确實無須使用動态代理,但在編寫架構或底層基礎代碼時,動态代理的作用非常大。

5.2 動态代理和 AOP

----------------------------------------------------------------------------

*

*

*

6 反射和泛型

-------------------------------------------------------------------------------------------------------------------------