詳解 Java 反射

反射(Reflection) 是 Java 程序開發語言的特徵之一,它允許運行中的 Java 程序對自身進行檢查,或者說“自審”,並能直接操作程序的內部屬性和方法。

反射是一項高級開發人員應該掌握的“黑科技”,其實反射並不是 Java 獨有的,許多編程語言都提供了反射功能。反射是所有註解實現的原理,尤其在框架設計中,有不可替代的作用。

關於反射,常見的知識點包括:

  • 如何反射獲取 Class 對象
  • 如何反射獲取類中的所有字段
  • 如何反射獲取類中的所有構造方法
  • 如何反射獲取類中的所有非構造方法

本篇我們就一起來學習一下 Java 反射機制。

一、反射是什麼?

反射的概念是由 Smith 在 1982 年首次提出的,主要是指程序可以訪問、檢測和修改它本身狀態或行爲的一種能力。

通俗地講,一提到反射,我們就可以想到鏡子。鏡子可以明明白白的照出我是誰,還可以照出別人是誰。反映到程序中,反射就是用來讓開發者知道這個類中有什麼成員,以及別的類中有什麼成員。

123.jpg

二、爲什麼要有反射

有的同學可能會疑惑,Java 已經有了封裝爲什麼還要有反射呢?反射看起來像是破壞了封裝性。甚至讓私有變量都可以被外部訪問到,使得類變得不那麼安全了。

我們來看一下 Oracle 官方文檔中對反射的描述:

Uses of Reflection

Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.

  • Extensibility Features

    An application may make use of external, user-defined classes by creating instances of extensibility objects using their fully-qualified names.

  • Class Browsers and Visual Development Environments

    A class browser needs to be able to enumerate the members of classes. Visual development environments can benefit from making use of type information available in reflection to aid the developer in writing correct code.

  • Debuggers and Test Tools

    Debuggers need to be able to examine private members on classes. Test harnesses can make use of reflection to systematically call a discoverable set APIs defined on a class, to insure a high level of code coverage in a test suite.

從 Oracle 官方文檔中可以看出,反射主要應用在以下幾方面:

  • 反射讓開發人員可以通過外部類的全路徑名創建對象,並使用這些類,實現一些擴展的功能。
  • 反射讓開發人員可以枚舉出類的全部成員,包括構造函數、屬性、方法。以幫助開發者寫出正確的代碼。
  • 測試時可以利用反射 API 訪問類的私有成員,以保證測試代碼覆蓋率。

也就是說,Oracle 希望開發者將反射作爲一個工具,用來幫助程序員實現本不可能實現的功能(perform operations which would otherwise be impossible)。正如《人月神話》一書中所言:軟件工程沒有銀彈。很多程序架構,尤其是三方框架,無法保證自己的封裝是完美的。如果沒有反射,對於外部類的私有成員,我們將一籌莫展,所以我們有了反射這一後門,爲程序設計提供了更大的靈活性。工具本身並沒有錯,關鍵在於如何正確的使用。

三、反射 API

Java 類的成員包括以下三類:屬性字段、構造函數、方法。反射的 API 也是與這幾個成員相關:

  1. Field 類:提供有關類的屬性信息,以及對它的動態訪問權限。它是一個封裝反射類的屬性的類。
  2. Constructor 類:提供有關類的構造方法的信息,以及對它的動態訪問權限。它是一個封裝反射類的構造方法的類。
  3. Method 類:提供關於類的方法的信息,包括抽象方法。它是用來封裝反射類方法的一個類。
  4. Class 類:表示正在運行的 Java 應用程序中的類的實例。
  5. Object類:Object 是所有 Java 類的父類。所有對象都默認實現了 Object 類的方法。

接下來,我們通過一個典型的例子來學習反射。

先做準備工作,新建 com.test.reflection 包,在此包中新建一個 Student 類:

package com.test.reflection;

public class Student {

    private String studentName;
    public int studentAge;

    public Student() {
    }

    private Student(String studentName) {
        this.studentName = studentName;
    }

    public void setStudentAge(int studentAge) {
        this.studentAge = studentAge;
    }

    private String show(String message) {
        System.out.println("show: " + studentName + "," + studentAge + "," + message);
        return "testReturnValue";
    }
}

可以看到,Student 類中有兩個字段、兩個構造方法、兩個函數,且都是一個私有,一個公有。由此可知,這個測試類基本涵蓋了我們平時常用的所有類成員。

3.1.獲取 Class 對象的三種方式

獲取 Class 對象有三種方式:

// 1.通過字符串獲取Class對象,這個字符串必須帶上完整路徑名
Class studentClass = Class.forName("com.test.reflection.Student");
// 2.通過類的class屬性
Class studentClass2 = Student.class;
// 3.通過對象的getClass()函數
Student studentObject = new Student();
Class studentClass3 = studentObject.getClass();
  • 第一種方法時通過類的全路徑字符串獲取Class對象,這也是我們平時最常用的反射獲取 Class 對象的方法;

  • 第二種方法有限制條件:需要導入類的包;

  • 第三種方法已經有了 Student 對象,不再需要反射。

通過這三種方式獲取到的 Class 對象是同一個,也就是說 Java 運行時,每一個類只會生成一個 Class 對象。

我們將其打印出來測試一下:

System.out.println("class1 = " + studentClass + "\n" +
        "class2 = " + studentClass2 + "\n" +
        "class3 = " + studentClass3 + "\n" +
        "class1 == class2 ? " + (studentClass == studentClass2) + "\n" +
        "class2 == class3 ? " + (studentClass2 == studentClass3));

運行程序,輸出如下:

class1 = class com.test.reflection.Student
class2 = class com.test.reflection.Student
class3 = class com.test.reflection.Student
class1 == class2 ? true
class2 == class3 ? true

OK,拿到 Class 對象之後,我們就可以爲所欲爲啦!

3.2.獲取成員變量

獲取字段有兩個 API:getDeclaredFieldsgetFields。他們的區別是: getDeclaredFields 用於獲取所有聲明的字段,包括公有字段和私有字段,getFields 僅用來獲取公有字段:

// 1.獲取所有聲明的字段
Field[] declaredFieldList = studentClass.getDeclaredFields();
for (Field declaredField : declaredFieldList) {
    System.out.println("declared Field: " + declaredField);
}
// 2.獲取所有公有的字段
Field[] fieldList = studentClass.getFields();
for (Field field : fieldList) {
    System.out.println("field: " + field);
}

運行程序,輸出如下:

declared Field: private java.lang.String com.test.reflection.Student.studentName
declared Field: public int com.test.reflection.Student.studentAge
field: public int com.test.reflection.Student.studentAge

3.3.獲取構造方法

獲取構造方法同樣包含了兩個 API:用於獲取所有構造方法的getDeclaredConstructors和用於獲取公有構造方法的getConstructors:

// 1.獲取所有聲明的構造方法
Constructor[] declaredConstructorList = studentClass.getDeclaredConstructors();
for (Constructor declaredConstructor : declaredConstructorList) {
    System.out.println("declared Constructor: " + declaredConstructor);
}
// 2.獲取所有公有的構造方法
Constructor[] constructorList = studentClass.getConstructors();
for (Constructor constructor : constructorList) {
    System.out.println("constructor: " + constructor);
}

運行程序,輸出如下:

declared Constructor: public com.test.reflection.Student()
declared Constructor: private com.test.reflection.Student(java.lang.String)
constructor: public com.test.reflection.Student()

3.4.獲取非構造方法

同樣地,獲取非構造方法的兩個 API 是:獲取所有聲明的非構造函數的 getDeclaredMethods和僅獲取公有非構造函數的getMethods

// 1.獲取所有聲明的函數
Method[] declaredMethodList = studentClass.getDeclaredMethods();
for (Method declaredMethod : declaredMethodList) {
    System.out.println("declared Method: " + declaredMethod);
}
// 2.獲取所有公有的函數
Method[] methodList = studentClass.getMethods();
for (Method method : methodList) {
    System.out.println("method: " + method);
}

運行程序,輸出如下:

declared Method: public void com.test.reflection.Student.setStudentAge(int)
declared Method: private java.lang.String com.test.reflection.Student.show(java.lang.String)
method: public void com.test.reflection.Student.setStudentAge(int)
method: public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method: public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
method: public final void java.lang.Object.wait() throws java.lang.InterruptedException
method: public boolean java.lang.Object.equals(java.lang.Object)
method: public java.lang.String java.lang.Object.toString()
method: public native int java.lang.Object.hashCode()
method: public final native java.lang.Class java.lang.Object.getClass()
method: public final native void java.lang.Object.notify()
method: public final native void java.lang.Object.notifyAll()

從輸出中我們看到,getMethods方法不僅獲取到了我們聲明的公有方法setStudentAge,還獲取到了很多 Object 類中的公有方法。這是因爲我們前文已說到:Object 是所有 Java 類的父類。所有對象都默認實現了 Object 類的方法。 而 getDeclaredMethods是無法獲取到父類中的方法的。

四、實踐

學以致用,讓我們來一個實際的應用感受一下。還是以 Student 類爲例,如果此類在其他的包中,並且我們的需求是要在程序中通過反射獲取他的構造方法,構造出 Student 對象,並且通過反射訪問他的私有字段和私有方法。那麼我們可以這樣做:

// 1.通過字符串獲取Class對象,這個字符串必須帶上完整路徑名
Class studentClass = Class.forName("com.test.reflection.Student");
// 2.獲取聲明的構造方法,傳入所需參數的類名,如果有多個參數,用','連接即可
Constructor studentConstructor = studentClass.getDeclaredConstructor(String.class);
// 如果是私有的構造方法,需要調用下面這一行代碼使其可使用,公有的構造方法則不需要下面這一行代碼
studentConstructor.setAccessible(true);
// 使用構造方法的newInstance方法創建對象,傳入構造方法所需參數,如果有多個參數,用','連接即可
Object student = studentConstructor.newInstance("NameA");
// 3.獲取聲明的字段,傳入字段名
Field studentAgeField = studentClass.getDeclaredField("studentAge");
// 如果是私有的字段,需要調用下面這一行代碼使其可使用,公有的字段則不需要下面這一行代碼
// studentAgeField.setAccessible(true);
// 使用字段的set方法設置字段值,傳入此對象以及參數值
studentAgeField.set(student,10);
// 4.獲取聲明的函數,傳入所需參數的類名,如果有多個參數,用','連接即可
Method studentShowMethod = studentClass.getDeclaredMethod("show",String.class);
// 如果是私有的函數,需要調用下面這一行代碼使其可使用,公有的函數則不需要下面這一行代碼
studentShowMethod.setAccessible(true);
// 使用函數的invoke方法調用此函數,傳入此對象以及函數所需參數,如果有多個參數,用','連接即可。函數會返回一個Object對象,使用強制類型轉換轉成實際類型即可
Object result = studentShowMethod.invoke(student,"message");
System.out.println("result: " + result);

程序的邏輯註釋已經寫得很清晰了,我們再梳理一下:

  1. 先用第一種全路徑獲取 Class 的方法獲取到了 Student 的 Class 對象

  2. 然後反射調用它的私有構造方法private Student(String studentName),構建出 newInstance

  3. 再將其公有字段 studentAge 設置爲 10

  4. 最後反射調用其私有方法show,傳入參數 “message”,並打印出這個方法的返回值。

其中,setAccessible函數用於動態獲取訪問權限,Constructor、Field、Method 都提供了此方法,讓我們得以訪問類中的私有成員。

運行程序,輸出如下:

show: NameA,10,message
result: testReturnValue

OK,這樣我們就基本把 Java 反射機制的重要內容都介紹完了,有收穫的同學就點個贊吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章