Java反射詳解篇--一篇入魂

淵渟嶽發表於2022-03-25

1.反射概述

Java程式在執行時操作類中的屬性和方法的機制,稱為反射機制。

一個關鍵點:執行時

一般我們在開發程式時,都知道自己具體用了什麼類,直接建立使用即可。但當你寫一些通用的功能時沒辦法在編寫時知道具體的型別,並且程式跑起來還會有多種型別的可能,則需要在執行時動態的去呼叫某個類的屬性和方法,這就必須使用反射來實現。

例子說明:

Father f = new Children();

編譯時變數f 為Father型別,執行時為Children型別;

public void demo(Object obj){
 // 不知道呼叫者傳什麼具體物件
    ……
}

編譯時demo方法引數型別為Object,一般有兩種做法

第一種做法是知道引數型別有哪幾種情況,可以使用instanceof運算子進行判斷,再利用強制型別轉換將其轉換成其執行時型別的變數即可。

第二種做法是編譯時根本無法預知該物件和類可能屬於哪些類,程式只依靠執行時資訊動態的來發現該物件和類的真實資訊,這就必須使用反射。

那反射是怎麼做到在執行時獲取類的屬性和方法的呢?

理解類的載入機制的應該知道,當java檔案編譯成.class檔案,再被載入進入記憶體之後,JVM自動生成一個唯一對應的Class物件,這個Class是一個具體的類,這個Class類就是反射學習的重點。反射的操作物件就是這個Class類,通過Class類來獲取具體類的屬性和方法。

2.Class類

Class 類是用於儲存類或介面屬性和方法資訊的類,就是儲存類資訊的類,它類名稱就叫 Class。

2.1.理解Class類

Class類和構造方法原始碼

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
 private final ClassLoader classLoader;
 
 private Class(ClassLoader loader) {
  classLoader = loader;
    }

 ……
}

簡單分析下Class類

  1. Class類和String類都是被final關鍵字修飾的類,是不可以被繼承的類;
  2. Class類支援泛型T,也就是說在編寫程式時可以做到:反射 + 泛型;
  3. Class類實現了序列化標記介面Serializable,既是Class類可以被序列化和反序列化;
  4. Class類不能被繼承,同時唯一的一個構造器還是私有的,因為設計之初就是讓JVM在類載入後傳入ClassLoader物件來建立Class物件(每個類或介面對應一個JVM自動生成Class物件),開發人員只是呼叫Class物件,並沒有直接例項化Class的能力。

Class物件的建立是在載入類時由 Java 虛擬機器以及通過呼叫類載入器中的defineClass 方法自動構造的,關於類的載入可以通過繼承ClassLoader來實現自定義的類載入器,本文著重講反射,在此不展開講類載入相關知識。

2.2.獲取Class物件的三種方式

方式一:常用方式,Class.forName("包名.類名")

public static void main(String[] args) {
    // 方式一:全限定類名字串
    Class<?> childrenClass = null;
    try {
        childrenClass = Class.forName("com.yty.fs.Children"); // 包名.類名
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    // 獲取類名稱
    System.out.println("全限定類名="+childrenClass.getName());
}

執行結果:全限定類名=com.yty.fs.Children

方式二:每個類下的靜態屬性 class,類名.class

public static void main(String[] args) {
    // 方式二:每個類下的靜態屬性 class
    Class<Children> childrenClass2 = Children.class;
    System.out.println("類名稱="+childrenClass.getSimpleName());
}

執行結果:類名稱=Children

方式三:每個類最終都繼承了Object,Object類下的getClass()

public static void main(String[] args) {
    // 方式三:Object類下的getClass()
    Children children = new Children();
    Class<?> childrenClass3 = children.getClass();
    System.out.println("類所在包="+childrenClass3.getPackage());
}

執行結果:類所在包=package com.yty.fs

三種方式簡單對比:

  • 方式一通過全限定類名字串既可以獲取,其他兩種方式都要匯入類Children才可以;
  • 方式二獲取的Class不需要強轉即可獲得指定型別Class,其他兩種方式獲得的都是未知型別Class<?>;
  • 方式三通過例項化物件的Object中的方法獲取,其他兩種都不需要例項化物件。

怎麼選:

  • 只有全限定類名字串,沒有具體的類可以匯入的只能選方式一;
  • 有具體類匯入沒有例項化物件的使用方式二;
  • 作為形參使用的使用方式三,通過形參引用來獲取Class。

Class類中有非常多的方法,通過案例掌握常用的方法即可。

2.3.案例一:構造方法、成員變數和成員方法的獲取和使用

1.構造方法操作

1.1.所有構造方法

1.2.所有public構造方法

1.3.無參構造方法

1.4.單個私有構造方法

2.欄位操作(成員變數)

2.1.獲取所有成員變數

2.2.獲取所有公共成員變數

2.3.獲取單個公共成員變數

2.4.獲取單個私有成員變數

3.方法操作(成員方法)

3.1.獲取所有方法--不會獲取父類的方法

3.2.獲取所有公共方法--會獲取父類的方法

3.3.獲取單個公共方法

3.3.1.獲取單個公共方法--無參方法

3.3.2.獲取單個公共方法--有參方法

3.4.獲取單個私有方法

具體看程式碼

測試類:Children類

public class Children {
    public String testString; //測試用
    private int id;
    private String name;

    // 無參構造方法
    public Children() {
        System.out.println("====無參構成方法被呼叫");
    }
    // 多個引數構造方法
    public Children(int id, String name) {
        this.id = id;
        this.name = name;
    }
    // default構造方法--測試
    Children(String name, int id){
        this.id = id;
        this.name = name;
    }
    // 受保護構造方法--測試
    protected Children(int id) {
        this.id = id;
    }
    // 私有構造方法--測試
    private Children(String name) {
        this.name = name;
    }
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

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

    public void printName(){
        System.out.println("====printName--"+this.name);
    }
    public void printName(String name){
        this.name = name;
        System.out.println("====printName--"+this.name);
    }
    private void demoTest(){
        System.out.println("====demoTest--執行了");
    }
}

Class類的具體操作

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * 案例一:構造方法、成員變數和成員方法的獲取和使用
 */public class Demo1 {
    public static void main(String[] args) throws Exception {
        Class<?> chilrenClass = Class.forName("com.yty.fs.Children");

        // 1.構造方法操作
        // 1.1.獲取所有構造方法
        System.out.println("1.構造方法操作\n1.1.所有構造方法");
        Constructor<?>[] declaredConstructors = chilrenClass.getDeclaredConstructors();
        for (Constructor constructor : declaredConstructors){
            System.out.println(constructor.toString()); // Constructor類的toString已重寫
        }

        // 1.2.獲取所有public構造方法
        System.out.println("1.2.所有public構造方法");
        Constructor<?>[] constructors = chilrenClass.getConstructors();
        for(Constructor constructor : constructors){
            System.out.println(constructor.toString());
        }

        // 1.3.獲取無參構造方法
        Constructor<?>  onParamConstructor = chilrenClass.getConstructor();//引數型別為null,表示無參
        System.out.println("1.3.無參構造方法:\n"+onParamConstructor.toString());
        // 例項化物件
        Object o = onParamConstructor.newInstance();
        if(o instanceof Children){
            Children children = (Children)o;
            children.setId(111);
            children.setName("myName");
            System.out.println(o.toString());// Children類重寫了toString
        }

        // 1.4.獲取單個私有構造方法
        // 指定了私有構造方法的引數型別,所以只會獲取到一個構造方法
        Constructor<?> privateConstructor = chilrenClass.getDeclaredConstructor(String.class);
        System.out.println("1.4.單個私有構造方法:\n"+privateConstructor.toString());
        //私有構造方法需要取消訪問許可權檢查,否則報異常:IllegalAccessExceptionw
        privateConstructor.setAccessible(true);
        Object obj = privateConstructor.newInstance("myName");
        System.out.println(o.toString());

        // 2.欄位操作(成員變數)
        // 2.1.獲取所有成員變數
        System.out.println("2.欄位操作(成員變數)\n2.1.獲取所有成員變數");
        Field[] declaredFields = chilrenClass.getDeclaredFields();
        for (Field declaredField : declaredFields){
            // 獲取fieldName
            System.out.println(declaredField.getName());
        }

        // 2.2.獲取所有公共成員變數
        System.out.println("2.2.獲取所有公共成員變數");
        Field[] fields = chilrenClass.getFields();
        for (Field field : fields){
            // 獲取fieldName
            System.out.println(field.getName());
        }

        // 2.3.獲取單個公共成員變數
        System.out.println("2.3.獲取單個公共成員變數");
        Field field = chilrenClass.getField("testString");
        Object o1 = chilrenClass.getConstructor().newInstance();
        field.set(o1,"yty");
        Object o1_1 = field.get(o1);
        // 獲取fieldName
        System.out.println("成員變數名-值:"+field.getName()+"="+o1_1.toString());

        // 2.4.獲取單個私有成員變數
        System.out.println("2.4.獲取單個私有成員變數");
        Field field2 = chilrenClass.getDeclaredField("name");
        //私有成員變數需要取消訪問許可權檢查,否則報異常:IllegalAccessExceptionw
        field2.setAccessible(true);
        Object o2 = chilrenClass.getConstructor().newInstance();
        field2.set(o2,"myName");
        Object o2_2 = field2.get(o2);
        // 獲取fieldName
        System.out.println("成員變數名-值:"+field2.getName()+"="+o2_2.toString());


        // 3.方法操作(成員方法)
        // 3.1.獲取所有方法(成員方法)
        System.out.println("3.方法操作(成員方法)\n3.1.獲取所有方法--不會獲取父類的方法");
        Method[] declaredMethods = chilrenClass.getDeclaredMethods();
        for (Method method : declaredMethods){
            // 獲取方法名
            System.out.println(method.getName());
        }

        // 3.2.獲取所有公共方法
        System.out.println("3.2.獲取所有公共方法--會獲取父類的方法");
        Method[] methods = chilrenClass.getMethods();
        for (Method method : methods){
            // 獲取方法名
            System.out.println(method.getName());
        }

        // 3.3.獲取單個公共方法
        System.out.println("3.3.獲取單個公共方法\n3.3.1.獲取單個公共方法--無參方法");
        Method printName = chilrenClass.getMethod("printName"); //方法名稱
        System.out.println(printName);

        System.out.println("3.3.2.獲取單個公共方法--有參方法");
        Method printName2 = chilrenClass.getMethod("printName",String.class); //方法名稱,引數型別
        System.out.println("引數個數:"+printName2.getParameterCount());
        // 遍歷所有引數資訊
        Parameter[] parameters = printName2.getParameters();
        for (int i=0;i<printName2.getParameterCount();i++){
            Parameter param = parameters[i];
            if(param.isNamePresent()){
                System.out.println("第"+ (i+1) +"個引數資訊");
                System.out.println("引數型別="+param.getType());
                System.out.println("引數名稱="+param.getName());
            }
        }
        // 使用有參方法
        Object o3 = chilrenClass.getConstructor().newInstance();
        printName2.invoke(o3,"myName");//傳入引數值、執行方法

        // 3.4.獲取單個私有方法
        System.out.println("3.4.獲取單個私有方法");
        Method demoTest = chilrenClass.getDeclaredMethod("demoTest");
        // 使用私有無參方法
        Object o4 = chilrenClass.getConstructor().newInstance();
        demoTest.setAccessible(true);
        demoTest.invoke(o4);

    }
}

執行結果:拷貝過去執行就知道了……

2.4.案例二:註解的相關操作

自定義一個測試用的註解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(value = RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)
public @interface PersonAnnotation {
    String name() default "myName";
}

註解使用類和測試用的main方法

import java.lang.reflect.Field;

public class PersonAnnotationDemo {
    @PersonAnnotation(name = "張三")
    private String name;
    private int age;

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

    public static void main(String[] args) throws Exception {
        Class<?> annotateClass = Class.forName("com.yty.fs.PersonAnnotationDemo");
        Object o = annotateClass.newInstance();
        System.out.println("PersonAnnotationDemo是否是註解類:"+annotateClass.isAnnotation());

        Field[] declaredFields = annotateClass.getDeclaredFields();
        for(Field field : declaredFields){
            // PersonAnnotationDemo類中的成員變數是否有 PersonAnnotation註解
            if (field.isAnnotationPresent(PersonAnnotation.class)){
                // 獲取成員變數中 單個PersonAnnotation註解
                PersonAnnotation annotation = field.getAnnotation(PersonAnnotation.class);
                // 獲取 多個PersonAnnotation註解
                // PersonAnnotation[] annotationsByType = field.getAnnotationsByType(PersonAnnotation.class);
                /**
                 * 相類似的獲取註解方法:getDeclaredAnnotation、getDeclaredAnnotationsByType、getAnnotations、getDeclaredAnnotations
                 */
                // 輸出註解中的值
                System.out.println("輸出註解中的值:"+field.getName()+"="+annotation.name());
                // 將註解的值 賦值 到PersonAnnotationDemo物件對應欄位
                field.setAccessible(true);//私有欄位需要忽略修飾符
                field.set(o,annotation.name());
            }
        }
        // 輸出:註解的值 賦值給 物件
        System.out.println(o.toString());

    }
}

執行結果:

PersonAnnotationDemo是否是註解類:false

輸出註解中的值:name=張三

PersonAnnotationDemo{name=張三, age=0}

3.反射的應用

常用於框架底層開發

3.1.實戰一:通過配置檔案解耦類和反射

Spring 框架通過將成員變數值以及依賴物件等資訊都放在配置檔案中進行管理的,類發生改變時只需要更新配置檔案,對於反射模組則無需更改,從而實現了較好的解耦。

測試類

public class BigBanana {

    public void printBigBanana(String color){
        System.out.println("Do you like "+color+" BigBanana?");
    }

}

配置檔案資訊

bigbanana.class.name=com.yty.fs.BigBanana
bigbanana.class.method=printBigBanana
bigbanana.class.method.param=java.lang.String

反射測試類

public class TestBigBanana {
    private static Properties properties;
    // 通過Key 獲取配置檔案value 值
    public static String getProperty(String key) throws IOException {
        if (properties == null){
            properties = new Properties();
            FileReader fileReader = new FileReader(new File("properties.properties"));
            properties.load(fileReader);
        }
        return properties.getProperty(key);
    }

    // 測試
    public static void main(String[] args) throws Exception {

        Class<?> aClass = Class.forName(getProperty("bigbanana.class.name"));
        Object o = aClass.getConstructor().newInstance();
        Class<?> paramClass = Class.forName(getProperty("bigbanana.class.method.param"));
        Method method = aClass.getMethod(getProperty("bigbanana.class.method"),paramClass);
        method.invoke(o,"yellow");
    }

}

執行結果:Do you like yellow BigBanana?

3.2.實戰二:代理賣手機--JDK動態代理

3.2.1.簡單理解代理模式

通過代理的方式(增加中間層)對想要訪問的類做一些控制,使程式碼職責清晰、通用化、智慧化、易擴充套件。

代理模式三個要點:一個公共介面、一個具體類(被代理類)、一個代理類

代理模式分為:靜態代理和動態代理

  • 靜態代理:代理類在編譯時已建立好具體類的物件,簡言之是幫你提前new好了物件;
  • 動態代理:代理類在程式執行時才建立具體類的物件,根據程式的執行不同可能呼叫的具體類不同。

JDK動態代理本質是通過反射來實現,涉及InvocationHandler介面和Proxy類。

Proxy類:建立動態代理例項;

InvocationHandler物件:當執行被代理物件裡的方法時,實際上會替換成呼叫InvocationHandler物件的invoke方法,動態的代理到介面下的實現類。

本次案例的類圖關係:

  • 綠色名為JDK動態代理關鍵介面和類
  • 紅色名為本案例要編寫的關鍵介面和類

image

3.2.2.手機介面

public interface Phone {
    void sellPhone();
}

3.2.3.三款手機類

華為

public class Huawei implements Phone {
    // 型號
    private String phoneModelName;

    public Huawei(String phoneModelName){
        this.phoneModelName=phoneModelName;
    }

    @Override
    public void sellPhone() {
        System.out.println("賣Huawei "+this.phoneModelName+" 的手機");
    }
}

小米

public class Xiaomi implements Phone{
    // 型號
    private String phoneModelName;

    public Xiaomi(String phoneModelName){
        this.phoneModelName=phoneModelName;
    }

    public Xiaomi(){

    }
    @Override
    public void sellPhone() {
        System.out.println("賣Xiaomi "+this.phoneModelName+" 的手機");
    }
}

愛瘋

public class IPhone implements Phone {
    // 型號
    private String phoneModelName;

    public IPhone(String phoneModelName){
        this.phoneModelName=phoneModelName;
    }

    @Override
    public void sellPhone() {
        System.out.println("賣IPhone "+this.phoneModelName+" 的手機");
    }
}

3.2.4.Invocation 實現類

public class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /**
         * 代理前可以考慮做些跟有趣的事
         */
        System.out.println("代理方法--start--代理前可以考慮做些跟有趣的事");
        Object invoke = method.invoke(target, args);
        /**
         * 代理後可能你有更想要做的事
         */
        System.out.println("代理方法--end--代理後可能你有更想要做的事\n");
        return invoke;
    }
}

3.2.5.代理商類

public class MyProxy {
    public static Object getProxy(Object target){
        MyInvocationHandler myInvocationHandler = new MyInvocationHandler(target);
        Object proxyInstance = Proxy.newProxyInstance(Phone.class.getClassLoader(), new Class[]{Phone.class}, myInvocationHandler);
        return proxyInstance;
    }
}

3.2.6.測試類

public class Test {

    public static void main(String[] args) {
        // 不管你想要買什麼手機,只要通過同一個代理商就可以買到
        Phone huawei = (Phone) MyProxy.getProxy(new Huawei("Huawei 16 pro"));
        Phone xiaomi = (Phone) MyProxy.getProxy(new Xiaomi("MI 13 pro"));
        Phone iphone = (Phone) MyProxy.getProxy(new IPhone("IPhone 13 pro"));
        huawei.sellPhone();
        xiaomi.sellPhone();
        iphone.sellPhone();
    }
}

測試結果:

代理方法--start--代理前可以考慮做些跟有趣的事

賣Huawei Huawei 16 pro 的手機

代理方法--end--代理後可能你有更想要做的事

代理方法--start--代理前可以考慮做些跟有趣的事

賣Xiaomi MI 13 pro 的手機

代理方法--end--代理後可能你有更想要做的事

代理方法--start--代理前可以考慮做些跟有趣的事

賣IPhone IPhone 13 pro 的手機

代理方法--end--代理後可能你有更想要做的事

4.反射與泛型的簡單實戰

通過實戰進一步理解泛型和反射。

4.1.實戰一:泛型方法和反射的結合

PrintResult類:注意成員方法私有

public class PrintResult {

    private void printSuccessInfo(){
        System.out.println("printSuccessInfo--執行成功");
    }
    private void printAdd(int[] ints){
        int n=0;
        for (int i :ints){
            n=n+i;
        }
        System.out.println("求和結果="+n);
    }
}

測試類

public class Demo {

    // 執行指定型別的方法
    public <T> void demo1(T t,String methodName,int... args) throws Exception {
        Class<?> tClass = t.getClass();
        Object o = tClass.getConstructor().newInstance();
        if (args == null){
            Method method = tClass.getDeclaredMethod(methodName);
            method.setAccessible(true);
            System.out.println("執行的方法="+method.getName());
            method.invoke(o);
        }else {
            Method method = tClass.getDeclaredMethod(methodName,int[].class);
            method.setAccessible(true);
            System.out.println("執行的方法="+method.getName());
            method.invoke(o,args);
        }
    }


    public static void main(String[] args) throws Exception {
        Demo demo = new Demo();
        PrintResult printResult = new PrintResult();
        demo.demo1(printResult,"printSuccessInfo",null);//執行printResult 無參方法
        demo.demo1(printResult,"printAdd",1888,2222,333);
    }
}

執行結果:

執行的方法=printSuccessInfo

printSuccessInfo--執行成功

執行的方法=printAdd

求和結果=4443

在此可以看到泛型T 的物件t,是可以像普通的物件一樣使用反射。

4.2.實戰二:通過反射越過泛型檢查

泛型在編譯期通過型別抹除機制來完成;

反射在執行期完成執行,可以理解為反射是在執行期將編譯好的list集合再新增元素進去。

public class Demo2 {

    public static void main(String[] args) throws Exception{
        List<String> list = new ArrayList<>();
        list.add("qwert");
        list.add("1234Z");
//        list.add(222); //指定了泛型型別為String後,無法add 非String的值

        Class listClass = list.getClass();
        //獲取和呼叫 list中的add()方法
        Method m = listClass.getMethod("add", Object.class);
        m.invoke(list, 100);

        //輸出List集合 -- toString已經被AbstractCollection重寫的了
        System.out.println("集合中的內容:"+list.toString());
    }
}

執行結果:集合中的內容:[qwert, 1234Z, 100]

image

Java往期文章

Java全棧學習路線、學習資源和麵試題一條龍

我心裡優秀架構師是怎樣的?

免費下載經典程式設計書籍

image

原創不易,三聯支援:點贊、分享

相關文章