反射Reflection、動態代理
尚硅谷JavaSE筆記-29

反射Reflection

  • 反射Reflection是Java被視為動態語言的關鍵,可以在運行時獲取類的內部訊息
    • 動態語言:程式運行時,代碼可以根據某些條件改變自身結構
  • 具體來說,當我們加載完某類之後,在記憶體中堆的方法區就產生了一個該類的Class物件,此物件包含了完整的該類的結構訊息
  • 因此我們能透過反射的API,從一個物件取得他所屬類的屬性與方法並進行各種操作

主要API

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

Class類

  • Class類即是"類的類",為反射的源頭
  • Object類中定義了getClass()方法,此方法被所有類繼承
  • Class類的實例物件只能由系統建立,當一個.class檔案被JVM載入執行時系統在堆的方法區產生唯一對應的物件
  • 通過Class物件可以完整獲取類的結構並進行各種操作,所以要使用反射必須先獲取Class物件
  • 萬物皆物件、萬物皆有Class類
    • 數組的話只要元素類型與維度相同,視為同一個Class,例如int[5]int[20]

獲取Class實例物件

  • 已知具體類名:Class clazz = String.class
  • 已知全類名:Class clazz = Class.forName("java.lang.String")
    • 可能拋出錯誤ClassNotFoundException
    • 也可以透過ClassLoader
      • ClassLoader cl = this.getClass().getClassLoader()
      • Class clazz4 = cl.loadClass("全類名")
  • 已有某類的實例物件:Class clazz = obj01.getClass()

ClassLoader

補充知識點,有印象就好

類的加載實際步驟
  1. Load:將.class檔案讀入記憶體、創建Class物件
  2. Link:將類的二進位數據合併到JRE中、引入常量
  3. Initialize:初始化,執行類構造器<clinit>()方法,進行static屬性或指定初始值的賦值
  • 如果一個類的父類還沒初始化,會先進行父類的初始化
  • JVM會保證<clinit>()方法在多線程環境中的加鎖與同步
加載器的分類
  • Bootstrap:引導類,由C語言編寫,負責Java平台核心庫,無法直接獲取
  • Extension:擴展類,負責載入jre/lib/ext或指定目錄下的jar包
  • System(APPs):系統類,負責載入java.class.path目錄下的jar包,最常用的
  • SomeClass:自訂類

從Class創立類的實例物件

  • 取得一個Class物件後,可以透過newInstance()方法創建對應類的實例,但有前提:
    • 該類必須有一個空參的構造器
    • 該構造器要有符合的訪問權限

從Class實例物件反射獲取資訊

基本都是getXXX(),沒特別聲明的會返回包含父類的public資訊

Declared用於指明"當前代",並且無視權限修飾符

取得構造器
  • public Constructor<T>[] getConstructors():返回public的構造器,構造器無法繼承,這可不會拿到父類的構造器
  • public Constructor<T>[] getDeclaredConstructors:返回當前類的所有構造器(含private)
  • 對於返回的Constructor類物件,還能再細探:
    • public int getModifiers():取得修飾符
    • public String getName():取得方法名稱
    • public Class<?>[] getParameterTypes():取得參數的類型
取得屬性
  • public Field[] getFields():返回類或接口public的Field(含父類)

  • public Field[] getDeclaredFields():返回當前的Field(含private)

  • 對於返回的Field類物件,還能再細探:

    • public int getModifiers():以整數形式返回此Field的修飾符

    • public Class<?> getType():得到Field的屬性類型

    • public String getName():返回Field的名稱

取得方法
  • public Method[] getMethods():返回類public的方法(含父類)
  • public Method[] getDeclaredMethods():返回當前的方法(含private)
  • 對於返回的Method類物件,還能再細探:
    • public Class<?> getReturnType():取得全部的返回值
    • public Class<?>[] getParameterTypes():取得全部的參數
    • public int getModifiers():取得修飾符
    • public Class<?>[] getExceptionTypes():取得異常資訊
獲取父類或實現的接口
  • public Class<?>[] getInterfaces():返回當前類所有實現的接口(不含父類)
  • public Class<? Super T> getSuperclass():返回父類的Class物件
  • 想獲得父類的接口可以2個組合用
其他資訊

框架常用到,尤其是泛型相關的

  • getDeclaredAnnotations():取得註解
  • getGenericSuperclass:獲取父類泛型類型Type
  • ParameterizedType:獲取泛型類型
  • getActualTypeArguments:獲取實際的泛型類型參數
  • Package getPackage():獲取類所在的包
範例

自訂一個Person類,他的繼承、接口、註解這邊就不貼了

用來嘗試調用上面講的這些獲得資訊方法

@MyAnnotation(value = "hi")
public class Person extends Creature<String> implements Comparable<String>, MyInterface {
    private String name;
    int age;
    public int id;

    public Person() {
    }

    @Override
    public String toString() {
        return "name=" + name + ",id=" + id+ ",age=" + age;
    }

    @MyAnnotation(value = "name&int")
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    private String show(String nation) {
        System.out.println("我的國籍是" + nation);
        return nation;
    }

    public static void display() {
        System.out.println("我是靜態方法");
    }


    @Override
    public int compareTo(String o) {
        return 0;
    }

    @Override
    public void info() {
        System.out.println("我是人");
    }

}

使用反射

獲取資訊後,可以用返回的結構透過反射來操作物件實例

操作屬性

作法1

已知要調用的屬姓名

Class<Person> clazz = Person.class;
Person p = clazz.newInstance();
// 返回該屬性的Field物件
Field id = clazz.getField("id");
// 調用set方法
id.set(p, 20);
System.out.println(p);

// 查看可調用get方法,返回obj需要強轉
int nid = (int) id.get(p);
System.out.println(nid);
  • getField(String 屬性名)方法獲得該屬性的Field物件
  • 以Field物件調用set方法
  • 受到權限管制,實際開發不這樣用

作法2

真正使用的

Class<Person> clazz = Person.class;
Person p = clazz.newInstance();
// 用Declared返回該屬性的Field物件
Field name = clazz.getDeclaredField("name");
// 開啟權限
name.setAccessible(true);
name.set(p, "Tom");
System.out.println(name.get(p));
  • 已知要調用的屬姓名
  • getDeclaredField(String 屬性名)方法獲得該屬性的Field物件,這次沒有權限問題了,總是能獲得
  • 但是能獲得不等於能修改,想修改之前要先setAccessible(true)
  • 以Field物件調用set方法

調用方法

使用invoke

Class<Person> clazz = Person.class;
Person p = clazz.newInstance();
// 調用方法,需要指明方法的形參類型
Method show = clazz.getDeclaredMethod("show", String.class);
show.setAccessible(true);
Object nation = show.invoke(p, "台灣");
System.out.println("nation = " + nation);
// 調用靜態方法
Method display = clazz.getDeclaredMethod("display");
display.invoke(clazz);
  • 已知要調用的方法名
  • getDeclaredMethod,需要指明具體形參類型,因為可能有很多override的方法
  • 有權限問題一樣需要setAccessible
  • invoke(物件, 形參...)的返回值就是調用的方法的返回值
    • 如果沒有則返回null
  • 也可以調用靜態方法

調用構造器

JDK8幾乎不這樣用,直接clazz.newInstance()就造物件了

但新版的JDK將該方法廢棄,建議用這種形式,所以還是學一下

Class<Person> clazz = Person.class;
// 調用構造器
Constructor<Person> con = clazz.getDeclaredConstructor(String.class, int.class);
con.setAccessible(true);
Person jay = con.newInstance("Jay", 50);
System.out.println(jay);
  • 參數的部分要注意,我本來還以為int.class要用Integer.class,結果不用

動態代理

一個動態的代理工廠,可以隨目標類創造出對應的代理實例

  • 由靜態代理模式轉動態,需要解決兩個問題

    • 如何根據加載到記憶體中的被代理類,動態的創建一個代理類實例物件?

    • 當通過代理類實例物件調用方法a時,如何動態的去調用被代理類中的同名方法a?

  • 範例代碼:

interface Human {
    String getBelief();

    void eat(String food);
}

// 被代理類
class SuperMan implements Human {

    @Override
    public String getBelief() {
        return "I believe I can fly!";
    }

    @Override
    public void eat(String food) {
        System.out.println("超人愛吃" + food);
    }
}

// 代理工廠
class ProxyFactory {
    // 調用此方法,返回一個代理類的物件
    public static Object getProxyInstance(Object obj) { // obj=被代理類的物件
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler();
        myInvocationHandler.bind(obj); // 連結物件
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces(),
                myInvocationHandler);
    }
}

// 代理類的物件被調用方法a時,就自動調用invoke方法
class MyInvocationHandler implements InvocationHandler {
    private Object obj; // 需要使用被代理類的物件進行賦值

    // 連結物件
    public void bind(Object obj) {
        this.obj = obj;
    }

    // 物件傳進來,在這裡進行反射調用
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 要執行的方法就在這
        return method.invoke(obj, args);
    }
}

public class ProxyTest {
    public static void main(String[] args) {
        SuperMan superMan = new SuperMan();
        Human proxyInstance = (Human) ProxyFactory.getProxyInstance(superMan);
        System.out.println(proxyInstance.getBelief());
        proxyInstance.eat("漢堡包");

    }
}
  • 實現InvocationHandler接口,在invoke方法中完成具體的代理操作
  • 透過Proxy.newProxyInstance靜態方法,返回代理類的物件
  • 以此物件完成想代理的操作

小結

  • 獲取Class物件:類.class
  • 反射創造實例:newInstance()
  • 獲取資訊:
    • getXXX()獲取public及一些父類的
    • getDeclaredXXX()獲得當前類,無視封裝性
  • 突破封裝權限:setAccessible(true)
  • 操作:set改變屬性、invoke調用方法
  • 動態代理:Proxy.newProxyInstance靜態方法、實現InvocationHandler接口

上次修改於 2021-12-14