Android 開源專案原始碼解析 -->公共技術點之 Java 反射 Reflection(十六)

Wei_Leng發表於2016-09-28

1. 瞭解 Java 中的反射

1.1 什麼是 Java 的反射

Java 反射是可以讓我們在執行時獲取類的函式、屬性、父類、介面等 Class 內部資訊的機制。通過反射還可以讓我們在執行期例項化物件,呼叫方法,通過呼叫 get/set 方法獲取變數的值,即使方法或屬性是私有的的也可以通過反射的形式呼叫,這種“看透 class”的能力被稱為內省,這種能力在框架開發中尤為重要。 有些情況下,我們要使用的類在執行時才會確定,這個時候我們不能在編譯期就使用它,因此只能通過反射的形式來使用在執行時才存在的類(該類符合某種特定的規範,例如 JDBC),這是反射用得比較多的場景。
還有一個比較常見的場景就是編譯時我們對於類的內部資訊不可知,必須得到執行時才能獲取類的具體資訊。比如 ORM 框架,在執行時才能夠獲取類中的各個屬性,然後通過反射的形式獲取其屬性名和值,存入資料庫。這也是反射比較經典應用場景之一。

1.2 Class 類

那麼既然反射是操作 Class 資訊的,Class 又是什麼呢?

這裡寫圖片描述

當我們編寫完一個 Java 專案之後,所有的 Java 檔案都會被編譯成一個.class 檔案,這些 Class 物件承載了這個型別的父類、介面、建構函式、方法、屬性等原始資訊,這些 class 檔案在程式執行時會被 ClassLoader 載入到虛擬機器中。當一個類被載入以後,Java 虛擬機器就會在記憶體中自動產生一個 Class 物件。我們通過 new 的形式建立物件實際上就是通過這些 Class 來建立,只是這個過程對於我們是不透明的而已。
下面的章節中我們會為大家演示反射的一些常用 api,從程式碼的角度理解反射。

2. 反射 Class 以及構造物件

2.1 獲取 Class 物件

在你想檢查一個類的資訊之前,你首先需要獲取類的 Class 物件。Java 中的所有型別包括基本型別,即使是陣列都有與之關聯的 Class 類的物件。如果你在編譯期知道一個類的名字的話,那麼你可以使用如下的方式獲取一個類的 Class 物件。

Class<?> myObjectClass = MyObject.class;

如果你已經得到了某個物件,但是你想獲取這個物件的 Class 物件,那麼你可以通過下面的方法得到:

Student me = new Student("mr.simple");
Class<?> clazz = me.getClass();

如果你在編譯期獲取不到目標型別,但是你知道它的完整類路徑,那麼你可以通過如下的形式來獲取 Class 物件:

Class<?> myObjectClass = Class.forName("com.simple.User");

在使用 Class.forName()方法時,你必須提供一個類的全名,這個全名包括類所在的包的名字。例如 User 類位於 com.simple 包,那麼他的完整類路徑就是 com.simple.User。
如果在呼叫 Class.forName()方法時,沒有在編譯路徑下(classpath)找到對應的類,那麼將會丟擲 ClassNotFoundException。

介面說明

// 載入指定的 Class 物件,引數 1 為要載入的類的完整路徑,例如"com.simple.Student". ( 常用方式 )
public static Class<?> forName (String className)

// 載入指定的 Class 物件,引數 1 為要載入的類的完整路徑,例如"com.simple.Student";
// 引數 2 為是否要初始化該 Class 物件,引數 3 為指定載入該類的 ClassLoader.
public static Class<?> forName (String className, boolean shouldInitialize, ClassLoader classLoader)

2.2 通過 Class 物件構造目標型別的物件

一旦你拿到 Class 物件之後,你就可以為所欲為了!當你善用它的時候它就是神兵利器,當你心懷鬼胎之時它就會變成惡魔。但獲取 Class 物件只是第一步,我們需要在執行那些強大的行為之前通過 Class 物件構造出該型別的物件,然後才能通過該物件釋放它的能量。 我們知道,在 java 中要構造物件,必須通過該類的建構函式,那麼其實反射也是一樣一樣的。但是它們確實有區別的,通過反射構造物件,我們首先要獲取類的 Constructor(構造器)物件,然後通過 Constructor 來建立目標類的物件。還是直接上程式碼的。

    private static void classForName() {
        try {
            // 獲取 Class 物件
            Class<?> clz = Class.forName("org.java.advance.reflect.Student");
            // 通過 Class 物件獲取 Constructor,Student 的建構函式有一個字串引數
            // 因此這裡需要傳遞引數的型別 ( Student 類見後面的程式碼 )
            Constructor<?> constructor = clz.getConstructor(String.class);
            // 通過 Constructor 來建立 Student 物件
            Object obj = constructor.newInstance("mr.simple");
            System.out.println(" obj :  " + obj.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

通過上述程式碼,我們就可以在執行時通過完整的類名來構建物件。

獲取建構函式介面

// 獲取一個公有的建構函式,引數為可變引數,如果建構函式有引數,那麼需要將引數的型別傳遞給 getConstructor 方法
public Constructor<T> getConstructor (Class...<?> parameterTypes)
// 獲取目標類所有的公有建構函式
public Constructor[]<?> getConstructors ()

注意,當你通過反射獲取到 Constructor、Method、Field 後,在反射呼叫之前將此物件的 accessible 標誌設定為 true,以此來提升反射速度。值為 true 則指示反射的物件在使用時應該取消 Java 語言訪問檢查。值為 false 則指示反射的物件應該實施 Java 語言訪問檢查。例如 :

   Constructor<?> constructor = clz.getConstructor(String.class);
   // 設定 Constructor 的 Accessible
   constructor.setAccessible(true);

   // 設定 Methohd 的 Accessible
   Method learnMethod = Student.class.getMethod("learn", String.class);
   learnMethod.setAccessible(true);

由於後面還會用到 Student 以及相關的類,我們在這裡就先給出它們的程式碼吧。
Person.java

public class Person {
    String mName;

    public Person(String aName) {
        mName = aName;
    }

    private void sayHello(String friendName) {
        System.out.println(mName + " say hello to " + friendName);
    }

    protected void showMyName() {
        System.out.println("My name is " + mName);
    }

    public void breathe() {
        System.out.println(" take breathe ");
    }
}

Student.java

public class Student extends Person implements Examination {
    // 年級
    int mGrade;

    public Student(String aName) {
        super(aName);
    }

    public Student(int grade, String aName) {
        super(aName);
        mGrade = grade;
    }

    private void learn(String course) {
        System.out.println(mName + " learn " + course);
    }

    public void takeAnExamination() {
        System.out.println(" takeAnExamination ");
    }

    public String toString() {
        return " Student :  " + mName;
    }

Breathe.java

// 呼吸介面
public interface Breathe {
    public void breathe();
}

Examination.java

// 考試介面
public interface Examination {
    public void takeAnExamination();
}

3 反射獲取類中函式

3.1 獲取當前類中定義的方法

要獲取當前類中定義的所有方法可以通過 Class 中的 getDeclaredMethods 函式,它會獲取到當前類中的 public、default、protected、private 的所有方法。而 getDeclaredMethod(String name, Class...<?> parameterTypes)則是獲取某個指定的方法。程式碼示例如下 :

 private static void showDeclaredMethods() {
      Student student = new Student("mr.simple");
        Method[] methods = student.getClass().getDeclaredMethods();
        for (Method method : methods) {
            System.out.println("declared method name : " + method.getName());
        }

        try {
            Method learnMethod = student.getClass().getDeclaredMethod("learn", String.class);
            // 獲取方法的引數型別列表
            Class<?>[] paramClasses = learnMethod.getParameterTypes() ;
            for (Class<?> class1 : paramClasses) {
                System.out.println("learn 方法的引數型別 : " + class1.getName());
            }
            // 是否是 private 函式,屬性是否是 private 也可以使用這種方式判斷
            System.out.println(learnMethod.getName() + " is private "
                    + Modifier.isPrivate(learnMethod.getModifiers()));
            learnMethod.invoke(student, "java ---> ");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

3.2 獲取當前類、父類中定義的公有方法

要獲取當前類以及父類中的所有 public 方法可以通過 Class 中的 getMethods 函式,而 getMethod 則是獲取某個指定的方法。程式碼示例如下 :

    private static void showMethods() {
        Student student = new Student("mr.simple");
        // 獲取所有方法
        Method[] methods = student.getClass().getMethods();
        for (Method method : methods) {
            System.out.println("method name : " + method.getName());
        }

        try {
            // 通過 getMethod 只能獲取公有方法,如果獲取私有方法則會丟擲異常,比如這裡就會拋異常
            Method learnMethod = student.getClass().getMethod("learn", String.class);
            // 是否是 private 函式,屬性是否是 private 也可以使用這種方式判斷
            System.out.println(learnMethod.getName() + " is private " + Modifier.isPrivate(learnMethod.getModifiers()));
            // 呼叫 learn 函式
            learnMethod.invoke(student, "java");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

介面說明

// 獲取 Class 物件中指定函式名和引數的函式,引數一為函式名,引數 2 為引數型別列表
public Method getDeclaredMethod (String name, Class...<?> parameterTypes)

// 獲取該 Class 物件中的所有函式( 不包含從父類繼承的函式 )
public Method[] getDeclaredMethods ()

// 獲取指定的 Class 物件中的**公有**函式,引數一為函式名,引數 2 為引數型別列表
public Method getMethod (String name, Class...<?> parameterTypes)

// 獲取該 Class 物件中的所有**公有**函式 ( 包含從父類和介面類整合下來的函式 )
public Method[] getMethods ()

這裡需要注意的是 getDeclaredMethod 和 getDeclaredMethods 包含 private、protected、default、public 的函式,並且通過這兩個函式獲取到的只是在自身中定義的函式,從父類中整合的函式不能夠獲取到。而 getMethod 和 getMethods 只包含 public 函式,父類中的公有函式也能夠獲取到。

4 反射獲取類中的屬性

獲取屬性和章節 3 中獲取方法是非常相似的,只是從 getMethod 函式換成了 getField,從 getDeclaredMethod 換成了 getDeclaredField 罷了。

4.1 獲取當前類中定義的屬性

要獲取當前類中定義的所有屬性可以通過 Class 中的 getDeclaredFields 函式,它會獲取到當前類中的 public、default、protected、private 的所有屬性。而 getDeclaredField 則是獲取某個指定的屬性。程式碼示例如下 :

    private static void showDeclaredFields() {
        Student student = new Student("mr.simple");
        // 獲取當前類和父類的所有公有屬性
        Field[] publicFields = student.getClass().getDeclaredFields();
        for (Field field : publicFields) {
            System.out.println("declared field name : " + field.getName());
        }

        try {
            // 獲取當前類和父類的某個公有屬性
            Field gradeField = student.getClass().getDeclaredField("mGrade");
            // 獲取屬性值
            System.out.println(" my grade is : " + gradeField.getInt(student));
            // 設定屬性值
            gradeField.set(student, 10);
            System.out.println(" my grade is : " + gradeField.getInt(student));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

4.2 獲取當前類、父類中定義的公有屬性

要獲取當前類以及父類中的所有 public 屬性可以通過 Class 中的 getFields 函式,而 getField 則是獲取某個指定的屬性。程式碼示例如下 :

    private static void showFields() {
        Student student = new Student("mr.simple");
        // 獲取當前類和父類的所有公有屬性
        Field[] publicFields = student.getClass().getFields();
        for (Field field : publicFields) {
            System.out.println("field name : " + field.getName());
        }

        try {
            // 獲取當前類和父類的某個公有屬性
            Field ageField = student.getClass().getField("mAge");
            System.out.println(" age is : " + ageField.getInt(student));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

介面說明

// 獲取 Class 物件中指定屬性名的屬性,引數一為屬性名
public Method getDeclaredField (String name)

// 獲取該 Class 物件中的所有屬性( 不包含從父類繼承的屬性 )
public Method[] getDeclaredFields ()

// 獲取指定的 Class 物件中的**公有**屬性,引數一為屬性名
public Method getField (String name)

// 獲取該 Class 物件中的所有**公有**屬性 ( 包含從父類和介面類整合下來的公有屬性 )
public Method[] getFields ()

這裡需要注意的是 getDeclaredField 和 getDeclaredFields 包含 private、protected、default、public 的屬性,並且通過這兩個函式獲取到的只是在自身中定義的屬性,從父類中整合的屬性不能夠獲取到。而 getField 和 getFields 只包含 public 屬性,父類中的公有屬性也能夠獲取到。

5 反射獲取父類與介面

5.1 獲取父類

獲取 Class 物件的父類。

    Student student = new Student("mr.simple");
    Class<?> superClass = student.getClass().getSuperclass();
    while (superClass != null) {
        System.out.println("Student's super class is : " + superClass.getName());
        // 再獲取父類的上一層父類,直到最後的 Object 類,Object 的父類為 null
        superClass = superClass.getSuperclass();
    }

5.2 獲取介面

獲取 Class 物件中實現的介面。

    private static void showInterfaces() {
        Student student = new Student("mr.simple");
        Class<?>[] interfaceses = student.getClass().getInterfaces();
        for (Class<?> class1 : interfaceses) {
            System.out.println("Student's interface is : " + class1.getName());
        }
    }

6 獲取註解資訊

在框架開發中,註解加反射的組合使用是最為常見形式的。關於註解方面的知識請參考公共技術點之 Java 註解 Annotation,定義註解時我們會通過@Target 指定該註解能夠作用的型別,看如下示例:

    @Target({
            ElementType.METHOD, ElementType.FIELD, ElementType.TYPE
    })
    @Retention(RetentionPolicy.RUNTIME)
    static @interface Test {

    }

上述註解的@target 表示該註解只能用在函式上,還有 Type、Field、PARAMETER 等型別,可以參考上述給出的參考資料。通過反射 api 我們也能夠獲取一個 Class 物件獲取型別、屬性、函式等相關的物件,通過這些物件的 getAnnotation 介面獲取到對應的註解資訊。 首先我們需要在目標物件上新增上註解,例如 :

@Test(tag = "Student class Test Annoatation")
public class Student extends Person implements Examination {
    // 年級
    @Test(tag = "mGrade Test Annotation ")
    int mGrade;

    // ......
}

然後通過相關的註解函式得到註解資訊,如下所示 :

    private static void getAnnotationInfos() {
        Student student = new Student("mr.simple");
        Test classTest = student.getClass().getAnnotation(Test.class);
        System.out.println("class Annotatation tag = " + classTest.tag());

        Field field = null;
        try {
            field = student.getClass().getDeclaredField("mGrade");
            Test testAnnotation = field.getAnnotation(Test.class);
            System.out.println("屬性的 Test 註解 tag : " + testAnnotation.tag());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

輸出結果為 : >

class Annotatation tag = Student class Test Annoatation
屬性的 Test 註解 tag : mGrade Test Annotation

介面說明

// 獲取指定型別的註解
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) ;
// 獲取 Class 物件中的所有註解
public Annotation[] getAnnotations() ;

雜談

反射作為 Java 語言的重要特性,在開發中有著極為重要的作用。很多開發框架都是基於反射來實現對目標物件的操作,而反射配合註解更是設計開發框架的主流選擇,例如 ActiveAndroid,因此深入瞭解反射的作用以及使用對於日後開發和學習必定大有益處。


相關文章