淺談Java中的反射

反射機制概述

靜態語言和動態語言:

  • 動態語言,是一類運行時可以改變其結構的語言:例如新的函數、對象甚至代碼可以被引進,已有的函數可以被刪除或是其他結構上的變化。通俗點說就是在運行時代碼可以根據某些條件改變自身結構。
    主要動態語言:Object-C、C#、JavaScript、PHP、Python等。
  • 靜態語言,是與動態語言相對的,運行時結構不可變的就是靜態語言。如 Java、C、C++。

Java不是動態語言,但是Java可以被稱爲"準動態語言"。即Java有一定的動態性,可以利用反射機制獲得類似動態語言的特性。Java的動態性讓編程的時候更加靈活。

反射

反射:Reflection(反射)是Java被視爲動態語言的關鍵,反射機制允許程序在執行期藉助Reflection API取得任何類的內部信息,並能直接操作任意對象的內部屬性及方法。
舉例:

Class c = Class.forName("java.lang.String");

通過上述語句,我們可以獲得一個Class類型 對象。

加載完類之後,在堆內存的方法區中就產生了一個Class類型的對象(一個類只有一個Class對象),這個對象包含了完整的類的結構信息。我們可以通過這個對象看到類的結構,這個對象就像一面鏡子,透過這個鏡子我們可以看到類的結構,所以,我們形象地稱之爲:反射。
在這裏插入圖片描述
反射機制提供的功能

  • 在運行時判斷任意一個對象所屬的類
  • 在運行時構造任意一個類的對象
  • 在運行時判斷任意一個類所具有的的成員變量和方法
  • 在運行時獲取泛型信息
  • 在運行時調用任意一個對象的成員變量和方法
  • 在運行時處理註解
  • 生成動態代理

反射優點和缺點

  • 優點:可以實現動態創建對象和編譯,體現出很大的靈活性
  • 缺點:對性能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麼並且它滿足我們的要求。這類操作總是慢於直接執行相同的操作。

反射相關的主要API

  • java.lang.Class:代表一個類
  • java.lang.reflect.Method:代表類的方法
  • java.lang.reflect.Field:代表類的成員變量
  • java.lang.reflect.Constructor:代表類的構造器

Class類

在Object類中定義了以下的方法,此方法將被所有子類繼承:

public final Class<?> getClass()

該方法返回此Object的運行時類也就是表示此對象運行時類的 Class 對象。返回的Class對象是由所表示類的static synchronized方法鎖定的對象。
返回的Class類是Java反射的源頭,我們也可以將反射理解爲:通過對象反射求出類的名稱

對於每個類而言,JRE都爲其保留了一個不變的Class類型的對象。一個Class對象包含了特定某個結構(class/interface/enum/annotation/primitive type/void/[])的有關信息。

Class類

public final class Class<T> extends Object 
implements Serializable, GenericDeclaration, Type, AnnotatedElement

Class 類的實例表示正在運行的 Java 應用程序中的類和接口。枚舉是一種類,註釋是一種接口。每個數組屬於被映射爲 Class 對象的一個類,所有具有相同元素類型和維數的數組都共享該 Class 對象。基本的 Java 類型(boolean、byte、char、short、int、long、float 和 double)和關鍵字 void 也表示爲 Class 對象。

Class 沒有公共構造方法。Class 對象是在加載類時由 Java 虛擬機以及通過調用類加載器中的 defineClass 方法自動構造的。

  • Class本身也是一個類
  • Class對象只能由系統建立對象
  • 一個加載的類在JVM中只會有一個Class實例
  • 一個Class對象對應的是一個加載到JVM中的一個.class文件
  • 每個類的實例都會記得自己是由哪個Class實例所生成
  • 通過Class可以完整地得到一個類中的所有被加載的結構
  • Class類是Reflection的根源,針對任何你想動態加載、運行的類,唯有先獲得相應的Class對象

Class類的常用方法:

方法名 具體含義
public static Class<?> forName(String className) 返回指定類名name的Class對象
public Object newInstance() 調用缺省構造方法,返回Class對象的一個實例
public String getName() 返回此Class對象所表示的實體(類,接口,數組類或void)的名稱
public Class<? super T> getSuperclass() 返回當前Class對象的父類的Class對象
public Class<?>[] getInterfaces() 獲取當前Class對象的接口
public ClassLoader getClassLoader() 返回該類的類加載器
public Constructor<?>[] getConstructors() 返回一個包含某些Constructor對象的數組
public Method[] getMethods() 返回一個包含某些 Method 對象的數組
public Method getMethod(String name,Class<?>… parameterTypes) 返回一個 Method 對象,此對象的形參類型爲paramType。它反映此 Class 對象所表示的類或接口的指定公共成員方法
public Field[] getDeclaredFields() 返回Field對象的一個數組

獲取Class類的實例

  • 若是已知具體的類,通過類的class屬性獲取。該方法是最爲安全可靠的,且程序性能最高。
public class Test02 {
    public static void main(String[] args) {
        Class class1 =  Student.class;
    }
}

class Student{
    private String name;
    private int age;
    
    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  • 若已知某個類的實例,可以調用該實例的getClass()方法獲取Class對象
public class Test02 {
    public static void main(String[] args) {
        Student student = new Student();
        Class class1 = student.getClass();
    }
}

class Student{
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  • 若已知一個類的全類名,且該類在類路徑下,可以通過Class類的靜態方法forName()獲取,可能會拋出ClassNotFoundException
package org.westos.demo2;

public class Test02 {
    public static void main(String[] args) {
        try {
            Class class1 = Class.forName("org.westos.demo2.Student");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

}

class Student{
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  • 內置基本數據類型可以直接使用 類名.Type
Class class1 = Integer.TYPE;
  • 還可以使用ClassLoader

有Class對象的類型

  • class:外部類,成員(成員內部類,靜態內部類),局部內部類,匿名內部類
  • interface:接口
  • []:數組
  • enum:枚舉
  • annotation:註解
  • primitive type:基本數據類型
  • void

類的加載與ClassLoader

當程序主動使用某個類時,如果該類還沒有被加載到內存中,則系統會通過以下三個步驟來對該類進行初始化:
在這裏插入圖片描述
具體分析:

  • 加載:將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區的運行時數據結構,然後生成一個代表這個類的java.lang.Class對象
  • 鏈接:將Java類的二進制代碼合併到JVM的運行狀態之中的過程
    驗證:確保加載的類信息符合JVM規範,沒有安全方面的問題
    準備:正式爲類變量(static 變量)分配內存並設置類變量默認初始值的階段,這些內存都將在方法區中進行分配
    解析:虛擬機常量池內的符號引用(常量名)替換爲直接引用(地址)的過程
  • 初始化:
    執行類構造器()方法的過程。類構造器()方法是由編譯期自動收集類中所有類變量的賦值動作和靜態代碼塊中的語句合併產生的。(類構造器是構造類信息的,不是構造該類對象的構造器)
    當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化
    虛擬機會保證一個類的()方法在多線程環境中被正確加鎖和同步

什麼時候會發生類初始化

  1. 類的主動引用(一定會發生類的初始化)
  • 當虛擬機啓動,先初始化main方法所在的類
  • new一個類的對象
  • 調用類的靜態成員(除了final常量)和靜態方法
  • 使用java.lang.reflect包的方法對類進行反射調用
  • 當初始化一個類,如果其父類沒有被初始化,則會先初始化它的父類
  1. 類的被動引用(不會發生類的初始化)
  • 當訪問一個靜態域時,只有真正聲明這個域的類纔會被初始化。如:當通過子類引用父類的靜態變量,不會導致子類初始化
  • 通過數組定義類引用,不會觸發此類的初始化
  • 引用常量不會觸發此類的初始化(常量在鏈接階段就存入調用類的常量池中了)

類記載器的作用

  • 類加載的作用:將class文件字節碼內容加載到內存中,並將這些靜態數據轉換成方法區的運行時數據結構,然後在堆中生成一個代表這個類的java.lang.Class對象,作爲方法區中類數據的訪問入口
  • 類緩存:標準的JavaSE類加載器可以按要求查找類,但一旦某個類被加載到類加載器中,它將維持加載(緩存)一段時間。不過JVM垃圾回收機制可以回收這些Class對象。

類加載器的類型
類加載器作用是用來把類(class)裝載進內存的。JVM 規範定義瞭如下類型的類的加載器。

  • 引導類加載器:用C++編寫的,是JVM自帶的類加載器,負責Java平臺核心庫,用來裝載核心類庫。該加載器無法直接獲取。
  • 擴展類加載器:負責jre/lib/ext目錄下的jar包或 - D java.ext.dirs 指定目錄下的jar包裝入工作。
  • 系統類加載器:負責java -classpath 或 - D java.class.path 所指的目錄下的類與jar包裝入工作,是最常用的加載器。
    在這裏插入圖片描述

創建運行時類的對象

我們可以通過反射來獲取運行時類的完整結構,如實現的全部接口、所繼承的父類、全部的構造器、全部的方法、全部的Field、註解等等。

創建類的對象的方法

創建類的對象的方法有兩種,一種是直接調用Class對象的newInstance()方法,另一種則是通過調用構造方法的newInstance()方法創建。

  • 調用Class對象的newInstance()方法
    1)類必須有一個無參數的構造器
    2)類的構造器的訪問權限需要足夠
package org.westos.demo2;

public class Test02 {
    public static void main(String[] args) throws Exception { 
            Class class1 = Class.forName("org.westos.demo2.Student");
            Student student = (Student) class1.newInstance();
    }

}

class Student{
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  • 通過明確地調用Class類的構造方法,並將參數傳遞進去
    1)通過Class類的getDeclaredConstructor(Class … parameterTypes)取得本類的指定形參類型的構造器
    2)向構造器的形參中傳遞一個對象數組進去,裏面包含了構造器中所需的各個參數
    3)通過Constructor實例化對象
public class Test02 {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("org.westos.demo2.Student");
        Constructor constructor = class1.getDeclaredConstructor(String.class, int.class);
        Student student = (Student) constructor.newInstance("Tom",23);
        System.out.println(student.getName());
    }

}

class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

調用指定的方法

創建完運行時類的對象之後,我們就可以通過反射,調用類中的方法,這是通過Method類完成。具體如下:

  • 通過Class類的getMethod(String name, Class … parameterTypes)方法取得一個Method對象,並設置此方法操作時所需要的參數類型。
public Method getMethod(String name, Class<?>... parameterTypes)
  • 之後使用Object invoke(Object obj, Object[] args) 進行調用,並向方法中傳遞要設置的obj對象的參數信息。
public Object invoke(Object obj, Object... args)

該方法用於調用某個類的對象的某個指定的方法。
第一個參數是對象(即需要調用哪個對象的方法),第二個參數是方法的參數。

  • Object對應原方法的返回值。若原方法無返回值,則此時返回null
  • 若原方法爲靜態方法,此時形參Object obj可爲null
  • 若原方法形參列表爲空,則Object[] args 爲null
  • 若原方法聲明爲private,則需要在調用此invoke()方法之前,顯式調用方法對象的setAccessible(true)方法,將可訪問private的方法。

舉例;

public class Test02 {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("org.westos.demo2.Student");
        Student student = (Student) class1.newInstance();
        Method setName = class1.getDeclaredMethod("setName", String.class);
        setName.invoke(student,"Lily");
        System.out.println(student.getName());
    }

}

class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

當原方法聲明爲private時,需要在調用invoke()方法前,顯式調用方法對象的setAccessible(true)方法,纔可訪問private的方法。

public class Test02 {
    public static void main(String[] args) throws Exception {
        Class class1 = Class.forName("org.westos.demo2.Student");
        Student student = (Student) class1.newInstance();
        Method setName = class1.getDeclaredMethod("setName", String.class);
        setName.setAccessible(true);
        setName.invoke(student,"Lily");
        System.out.println(student.getName());
    }

}

class Student {
    private String name;
    private int age;

    public Student() {
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    private void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

setAccessible()方法

public void setAccessible(boolean flag)
  • Method、Field和Constructor對象都有setAccessible()方法
  • setAccessible的作用是啓動和禁用訪問安全檢查的開關
  • 參數值爲true則指示反射的對象在使用時應該取消Java語言訪問檢查
    提高反射的效率。如果代碼中必須使用反射,而該句代碼需要頻繁地被調用,那麼就設置爲true
    設置爲true使得原本無法訪問的私有成員也可以訪問
  • 參數值爲false則指示反射的對象應該實施Java語言訪問檢查

反射操作泛型

Java採用泛型擦除的機制來引入泛型,Java中的泛型僅僅是給編譯器javac使用的,確保數據的安全性和免去強制類型轉換的問題,但是,一旦編譯完成,所有和泛型有關的類型全部擦除。

爲了通過反射操作這些類型,Java新增了ParameterizedType,GenericArrayType,TypeVariable和WildcardType幾種類型來代表不能被歸一化到Class類中的類型但是又和原始類型齊名的類型。

  • ParemeterizedType:表示一種參數化類型,如Collection
  • GenericArrayType:表示一種元素類型是參數化類型或者類型變量的數組類型
  • TypeVariable:是各種類型變量的公共父接口
  • WildcardType:代表一種通配符類型表達式
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章