Java高階語法之反射

張雷鋒發表於2022-01-27

Java高階語法之反射

什麼是反射

java.lang包提供java語言程式設計的基礎類,在lang包下存在一個子包:reflect,與反射相關的APIs均在此處;

官方對reflect包的介紹如下:

Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.
The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.

java.lang.reflect官方介紹

簡單來說,反射機制就像類對照平靜的湖面,湖面對映出類的欄位、方法、建構函式等資訊;反射機制不僅可以看到類資訊,還能針對欄位、方法等做出相應的操作。

測試實體類建立

為方便解釋說明,首先建立一個實體類,用於測試使用

package cn.byuan.entity;

/**
 * 學生實體類
 *
 * @author byuan
 * @date 2022-01-25
 */
public class Student {

    /**
     * 學生學號, 公共變數, 預設值: defaultStudentNo
     * */
    public String studentNo = "defaultStudentNo";

    /**
     * 學生姓名, 公共變數, 預設值: defaultStudentName
     * */
    public String studentName = "defaultStudentName";

    /**
     * 學生性別, 私有變數, 預設值: defaultStudentSex
     * */
    private String studentSex = "defaultStudentSex";

    /**
     * 學生年齡, 私有變數, 預設值: 0
     * */
    private Integer studentAge = 0;

    /**
     * 公有無參構造方法
     * */
    public Student() {

    }

    /**
     * 公有滿參構造方法
     * */
    public Student(String studentNo, String studentName, String studentSex, Integer studentAge) {
        this.studentNo = studentNo;
        this.studentName = studentName;
        this.studentSex = studentSex;
        this.studentAge = studentAge;
    }

    /**
     * 私有構造方法
     * */
    private Student(String studentSex, Integer studentAge) {
        this.studentSex = studentSex;
        this.studentAge = studentAge;
    }

    public String getStudentNo() {
        return studentNo;
    }

    public Student setStudentNo(String studentNo) {
        this.studentNo = studentNo;
        return this;
    }

    public String getStudentName() {
        return studentName;
    }

    public Student setStudentName(String studentName) {
        this.studentName = studentName;
        return this;
    }

    public String getStudentSex() {
        return studentSex;
    }

    public Student setStudentSex(String studentSex) {
        this.studentSex = studentSex;
        return this;
    }

    public Integer getStudentAge() {
        return studentAge;
    }

    public Student setStudentAge(Integer studentAge) {
        this.studentAge = studentAge;
        return this;
    }

    @Override
    public String toString() {
        return "Student{" +
                "studentNo = " + this.studentNo + ", " +
                "studentName = " + this.studentName + ", " +
                "studentSex = " + this.studentSex + ", " +
                "studentAge = " + this.studentAge +"}";
    }

    /**
     * 學生類說話方法
     * */
    private String speak(String message) {
        return this.studentName + " : " + message;
    }

}

反射中的幾個重要類及方法

在瞭解反射前,先來梳理下一個類(Class)本身中包含了哪些內容。

  1. 欄位,欄位又由修飾符、欄位型別、欄位名稱、對應值等部分組成
  2. 構造方法,構造方法可簡單分為無參與有參兩大類
  3. 非構造方法,又稱普通方法,方法由修飾符、返回值型別、方法名、形參表、方法體等部分構成

本文所論述的反射中幾個重要類及其對應方法正基於以上內容

Class類

Class類代表了類本身,類本身包含欄位,構造方法,非構造方法等內容,因此使用反射的第一步就是獲取物件所對應的Class類。

僅就使用反射而言,我們需著重瞭解Class類的獲取方式,下面給出例項

Class類例項

package cn.byuan.example;

import cn.byuan.entity.Student;

/**
 * 獲取 Class 的幾種方式
 *
 * @author byuan
 * @date 2022-01-25
 */
public class GetClassExample {

    public static void main(String[] args) throws ClassNotFoundException {

        // 獲取 class 方式一: 通過類的全路徑字串獲取 Class 物件
        Class getClassExample1 = Class.forName("cn.byuan.entity.Student");

        // 獲取 class 方式二: 通過類名直接獲取
        Class getClassExample2 = Student.class;

        // 獲取 class 方式三: 通過已建立的物件獲取對應 Class
        Student student1 = new Student();
        Class getClassExample3 = student1.getClass();

    }

}

Field類的獲取與常用屬性

Class類為我們提供了兩個方法用以獲取Field類:

  1. getDeclaredFields: 獲取所有宣告的欄位(包括公有欄位和私有欄位)
  2. getFields: 僅可獲取公有欄位

Field類代表了類中的屬性欄位,類中屬性欄位可分為兩種,公有欄位(public)與私有欄位(private);

每個欄位又具有四個屬性:修飾符,欄位型別,欄位名稱,對應值;Field也自然提供了對應方法對這四種屬性進行獲取:

  1. getModifiers():獲取欄位修飾符相加值,想要獲取明確標識需要通過Modifier常量的toString方法對相加值進行解碼
  2. getType():獲取欄位型別
  3. getName():獲取欄位名稱
  4. get(Object):獲取欄位對應值

下面給出例項:

Field類例項

package cn.byuan.example;

import cn.byuan.entity.Student;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

/**
 * 獲取類欄位的幾種方式
 *
 * @author byuan
 * @date 2021-01-25
 */
public class GetFieldExample {

    public static void main(String[] args) throws IllegalAccessException {

        Class studentClass = Student.class;

        // 獲取類欄位的兩個方法: getDeclaredFields, getFields

        // 1. getDeclaredFields: 獲取所有宣告的欄位(包括公有欄位和私有欄位)
        Field[] declaredFieldArray = studentClass.getDeclaredFields();
        printFieldInformation(declaredFieldArray);

        // 2. getFields: 僅可獲取公有欄位
        Field[] fieldArray = studentClass.getFields();
        printFieldInformation(fieldArray);

        // 獲取欄位對應值
        Student student = new Student()
                .setStudentSex("女")
                .setStudentAge(18);
        printFieldValue(student);

    }

    /**
     * 列印類欄位資訊
     *
     * @param fieldArray 類欄位物件列表
     * */
    public static void printFieldInformation(Field[] fieldArray) {

        for (Field fieldPart : fieldArray) {

            System.out.println("直接列印類欄位物件: " + fieldPart);

            // 獲取欄位修飾符
            String fieldModifier = Modifier.toString(fieldPart.getModifiers());

            // 獲取欄位型別
            String fieldType = fieldPart.getType().getName();

            // 獲取欄位名稱
            String fieldName = fieldPart.getName();

            System.out.println(fieldModifier + " " + fieldType + " " + fieldName);

        }

        System.out.println();

    }

    /**
     * 列印類欄位屬性
     *
     * @param t 泛型物件
     * */
    private static <T> void printFieldValue(T t) throws IllegalAccessException {
        Field[] fieldValueArray = t
                .getClass()
                .getDeclaredFields();

        for (Field fieldValuePart : fieldValueArray) {
            // 對於有可能存在的 private 欄位取消語言訪問檢查
            fieldValuePart.setAccessible(true);

            // 欄位名稱
            String filedName = fieldValuePart.getName();

            // 欄位對應值
            String fieldValue = fieldValuePart.get(t).toString();

            System.out.println(filedName + " = " + fieldValue);

        }

    }

}

執行截圖

Constructor類獲取與常用屬性

Class類為我們提供了兩個方法用以獲取Constructor類:

  1. getDeclaredConstructors():獲取所有構造方法
  2. getConstructors():僅可獲取公有構造方法

對於構造器,可簡單將其分為兩大類,無參構造器與帶參構造器,構造器也是方法的一種,因此對於任意一構造器都由修飾符,方法名,形參表,方法體四部分組成;Constructor自然也為其組成部分提供了對應方法:

  1. 與欄位類似,Constructors同樣提供了getModifiers()方法:獲取構造器修飾符相加值,想要獲取明確標識需要通過Modifier常量的toString方法進行解碼
  2. getName():獲取構造器名稱
  3. getParameterTypes():獲取構造器引數列表

下面給出例項

Constructor類例項

package cn.byuan.example;

import cn.byuan.entity.Student;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;

/**
 * 獲取構造方法的幾種方式
 *
 * @author byuan
 * @date 2022-01-25
 */
public class GetConstructorsExample {

    public static void main(String[] args) {

        Class studentClass = Student.class;

        // 獲取類構造器的兩個方法: getDeclaredConstructors, getConstructors

        // 1. getDeclaredConstructors: 獲取所有構造方法
        Constructor[] declaredConstructorArray = studentClass.getDeclaredConstructors();
        printConstructorInformation(declaredConstructorArray);

        // 2. getConstructors, 僅可獲取公有構造方法
        Constructor[] constructorArray = studentClass.getConstructors();
        printConstructorInformation(constructorArray);

    }

    /**
     * 列印構造器資訊
     *
     * @param constructorArray 構造器物件列表
     * */
    public static void printConstructorInformation(Constructor[] constructorArray) {

        for (Constructor constructorPart : constructorArray) {

            System.out.println("直接列印構造器物件: " + constructorPart);

            // 獲取構造器修飾符
            String constructorModifier = Modifier.toString(constructorPart.getModifiers());

            // 獲取構造器名稱
            String constructorName = constructorPart.getName();

            // 獲取構造器引數列表
            Class[] constructorParameterArray = constructorPart.getParameterTypes();

            // 列印構造器引數列表
            System.out.print(constructorModifier + " " + constructorName + "(");
            for (Class constructorParameterPart : constructorParameterArray) {
                System.out.print(constructorParameterPart.getName() + " ");
            }
            System.out.println(")");

        }

        System.out.println();
        
    }

}

執行截圖

利用Constructor類例項化物件

一般而言,我們關心的不只是獲取到物件構造器的具體資訊,更重要的是如何利用反射例項化物件,針對物件例項化,Constructor提供了兩種型別的方法:

  1. getConstructor(Class<?>... parameterTypes):獲取指定形參表的構造方法,可藉助其獲取無參/指定引數的構造方法;

  2. newInstance(Object ... initargs):通過形參表傳遞例項化物件引數,與getConstructor配合使用;

    注意:Class也存在newInstance()方法,但僅能用來呼叫類的無參構造器

Constructor例項化物件例項

package cn.byuan.example;

import cn.byuan.entity.Student;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

/**
 * 構造器呼叫例項
 *
 * @author byuan
 * @date 2022-01-26
 */
public class ConstructorsInvokeExample {

    public static void main(String[] args) throws InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {

        Class studentClass = Student.class;
        
        // Class 類 的 newInstance 方法
        studentClass.newInstance();
        
        // 1. 呼叫公有無參構造器
        Object object1 = studentClass
                .getConstructor()
                .newInstance();
        System.out.println(object1.getClass());

        // 2. 呼叫公有滿參構造器
        Constructor studentConstructorFull = studentClass
                .getConstructor(String.class, String.class, String.class, Integer.class);

        Object object2 = studentConstructorFull
                .newInstance("2022001", "趙一", "男", 18);
        System.out.println(object2);

        // 3. 呼叫私有構造器
        Constructor studentConstructorPrivate = studentClass
                .getDeclaredConstructor(String.class, Integer.class);

        // 私有構造器需將 accessible 設定為 true, 取消語言訪問檢查
        studentConstructorPrivate.setAccessible(true);
        Object object3 = studentConstructorPrivate
                .newInstance("女", 19);
        System.out.println(object3);

    }

}

執行截圖

Method類的獲取與常用屬性

Class類為我們提供了兩個方法用以獲取Method類:

  1. getDeclaredMethods():獲取所有非構造方法
  2. getMethods():僅可獲取公有非構造方法

對於任意方法都可分為修飾符,方法名,形參表,方法體四部分;Method為其組成部分提供了對應方法:

  1. getModifiers():獲取方法修飾符相加值,想要獲取明確標識需要通過Modifier常量的toString方法進行解碼
  2. getName():獲取方法名稱
  3. getParameterTypes():獲取方法形參表

Method類例項

package cn.byuan.example;

import cn.byuan.entity.Student;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * 獲取非構造方法的幾種方式
 *
 * @author byuan
 * @date 2022-01-26
 */
public class GetMethodExample {

    public static void main(String[] args) {

        Class studentClass = Student.class;

        // 獲取非構造方法的兩個方法: getDeclaredMethods, getMethods

        // 1. getDeclaredMethods: 獲取所有非構造方法
        Method[] declaredMethodArray = studentClass.getDeclaredMethods();
        printMethodInformation(declaredMethodArray);

        // 2. getMethods, 僅可獲取公有非構造方法
        Method[] methodArray = studentClass.getMethods();
        printMethodInformation(methodArray);

    }

    /**
     * 列印非構造器方法資訊
     *
     * @param methodArray 構造器物件列表
     * */
    public static void printMethodInformation(Method[] methodArray) {

        for (Method methodPart : methodArray) {

            System.out.println("直接列印非構造方法物件: " + methodArray);

            // 獲取非構造器方法修飾符
            String methodModifier = Modifier.toString(methodPart.getModifiers());

            // 獲取非構造器方法名稱
            String methodName = methodPart.getName();

            String methodReturnType = methodPart.getReturnType().getName();

            // 獲取非構造方法引數列表
            Class[] constructorParameterArray = methodPart.getParameterTypes();

            // 列印非構造方法引數列表
            System.out.print(methodModifier + " " + methodReturnType + " " + methodName + "(");
            for (Class methodParameterPart : constructorParameterArray) {
                System.out.print(methodParameterPart.getName() + " ");
            }
            System.out.println(")");

        }

        System.out.println();

    }

}

執行截圖

利用Method呼叫非構造方法

與利用Constructor例項化物件相似,Method同樣需要兩個方法用以呼叫非構造方法

  1. getDeclaredMethod(方法名, 形參表陣列): 獲取所有構造方法
  2. invoke(物件例項, 引數陣列):方法呼叫

Method呼叫非構造方法例項

package cn.byuan.example;

import cn.byuan.entity.Student;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 非構造器方法呼叫例項
 *
 * @author byuan
 * @date 2022-01-26
 */
public class MethodInvokeExample {

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {

        Class studentClass = Student.class;

        // 對於私有的非構造方法, 需要使用 getDeclaredMethod 進行獲取
        // getDeclaredMethod(方法名, 形參表陣列)
        Method studentSpeakMethod = studentClass.getDeclaredMethod("speak", new Class[]{String.class});
        
        // 取消語言訪問檢查
        studentSpeakMethod.setAccessible(true);
        
        // invoke(物件例項, 引數陣列)
        Object object = studentSpeakMethod.invoke(studentClass.newInstance(), "Hello, world");

        System.out.println(object);

    }

}

執行截圖

例項:利用反射機制編寫物件拷貝工具類

在實際專案中,我們經常會遇到POJO與VO等型別物件進行相互轉換的情況,如果直接進行轉換則會使用大量的樣板程式碼,為消除這樣的程式碼,我們可以寫一個簡單的物件拷貝工具類進行解決;

業務分析

通過反射獲取源物件Field陣列,並利用Field類提供的set/get方法實現同名屬性的拷貝;

建立兩個具有相同屬性的物件:Student與StudentOut

Student類
package cn.byuan.entity;

/**
 * 學生實體類
 *
 * @author byuan
 * @date 2022-01-25
 */
public class Student {

    /**
     * 學生學號, 公共變數, 預設值: defaultStudentNo
     * */
    public String studentNo = "defaultStudentNo";

    /**
     * 學生姓名, 公共變數, 預設值: defaultStudentName
     * */
    public String studentName = "defaultStudentName";

    /**
     * 學生性別, 私有變數, 預設值: defaultStudentSex
     * */
    private String studentSex = "defaultStudentSex";

    /**
     * 學生年齡, 私有變數, 預設值: 0
     * */
    private Integer studentAge = 0;

    /**
     * 公有無參構造方法
     * */
    public Student() {

    }

    /**
     * 公有滿參構造方法
     * */
    public Student(String studentNo, String studentName, String studentSex, Integer studentAge) {
        this.studentNo = studentNo;
        this.studentName = studentName;
        this.studentSex = studentSex;
        this.studentAge = studentAge;
    }

    /**
     * 私有構造方法
     * */
    private Student(String studentSex, Integer studentAge) {
        this.studentSex = studentSex;
        this.studentAge = studentAge;
    }

    public String getStudentNo() {
        return studentNo;
    }

    public Student setStudentNo(String studentNo) {
        this.studentNo = studentNo;
        return this;
    }

    public String getStudentName() {
        return studentName;
    }

    public Student setStudentName(String studentName) {
        this.studentName = studentName;
        return this;
    }

    public String getStudentSex() {
        return studentSex;
    }

    public Student setStudentSex(String studentSex) {
        this.studentSex = studentSex;
        return this;
    }

    public Integer getStudentAge() {
        return studentAge;
    }

    public Student setStudentAge(Integer studentAge) {
        this.studentAge = studentAge;
        return this;
    }

    @Override
    public String toString() {
        return "Student{" +
                "studentNo = " + this.studentNo + ", " +
                "studentName = " + this.studentName + ", " +
                "studentSex = " + this.studentSex + ", " +
                "studentAge = " + this.studentAge +"}";
    }

    /**
     * 學生類說話方法
     * */
    private String speak(String message) {
        return this.studentName + " : " + message;
    }

}

StudentOut類
package cn.byuan.api.out;

import cn.byuan.entity.Student;

/**
 * 學生類出參
 *
 * @author byuan
 * @date 2022-01-26
 */
public class StudentOut {
    /**
     * 學生學號, 公共變數
     * */
    private String studentNo;

    /**
     * 學生姓名, 公共變數
     * */
    private String studentName;

    /**
     * 學生性別, 私有變數
     * */
    private String studentSex;

    /**
     * 學生年齡, 私有變數
     * */
    private Integer studentAge;

    public String getStudentNo() {
        return studentNo;
    }

    public StudentOut setStudentNo(String studentNo) {
        this.studentNo = studentNo;
        return this;
    }

    public String getStudentName() {
        return studentName;
    }

    public StudentOut setStudentName(String studentName) {
        this.studentName = studentName;
        return this;
    }

    public String getStudentSex() {
        return studentSex;
    }

    public StudentOut setStudentSex(String studentSex) {
        this.studentSex = studentSex;
        return this;
    }

    public Integer getStudentAge() {
        return studentAge;
    }

    public StudentOut setStudentAge(Integer studentAge) {
        this.studentAge = studentAge;
        return this;
    }

    @Override
    public String toString() {
        return "StudentOut{" +
                "studentNo = " + this.studentNo + ", " +
                "studentName = " + this.studentName + ", " +
                "studentSex = " + this.studentSex + ", " +
                "studentAge = " + this.studentAge +"}";
    }
}

編寫工具類
package cn.byuan.util;

import cn.byuan.api.out.StudentOut;
import cn.byuan.entity.Student;

import java.lang.reflect.Field;

/**
 * 物件屬性拷貝工具類
 *
 * @author byuan
 * @date 2022-01-26
 */
public class BeanUtil {

    /**
     * 物件拷貝工具
     *
     * @param sourceObject 源物件
     * @param destClass 目的物件對應 Class
     *
     * @return 拷貝完畢後物件
     * */
    public static <T> T copyObject(Object sourceObject, Class<T> destClass) {

        if (sourceObject == null) {
            return null;

        }

        try {

            T destObject = destClass.newInstance();

            copyField(sourceObject, destObject);

            return destObject;

        } catch (InstantiationException e) {
            e.printStackTrace();

        } catch (IllegalAccessException e) {
            e.printStackTrace();

        } catch (Exception e) {
            e.printStackTrace();

        }

        return null;

    }


    /**
     * 物件屬性拷貝工具
     *
     * @param sourceObject 源物件
     * @param destObject 目的物件
     * */
    private static void copyField(Object sourceObject, Object destObject) {

        if (sourceObject == null || destObject == null) {
            return;
        }

        // 獲取源物件所有欄位
        Field[] sourceFieldArray = sourceObject.getClass().getDeclaredFields();
        for (Field sourceFieldPart : sourceFieldArray) {
            // 取消語言訪問檢查
            sourceFieldPart.setAccessible(true);

            String sourceFieldName = sourceFieldPart.getName();

            try {
                // 根據屬性名稱獲取目標物件對應類欄位
                Field destField = destObject
                        .getClass()
                        .getDeclaredField(sourceFieldName);

                destField.setAccessible(true);

                destField.set(destObject, sourceFieldPart.get(sourceObject));

            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

    }

    public static void main(String[] args) {

        Student student = new Student("2022001", "趙一", "男", 18);

        StudentOut studentOut = copyObject(student, StudentOut.class);

        System.out.println(studentOut);

    }

}

相關文章