1. 程式人生 > 實用技巧 >Java 語法 -- 反射

Java 語法 -- 反射

什麼是反射?

反射就是Reflection,Java的反射是指程式在執行期可以拿到一個物件的所有資訊。

反射的作用

反射是為了解決在執行期,對某個例項一無所知的情況下,如何呼叫其方法。

Class 類

class 是一個 java 關鍵字,表示宣告一個類。

除了int等基本型別外,Java的其他型別全部都是class(包括interface)。

class是由JVM在執行過程中動態載入的。JVM在第一次讀取到一種class型別時,將其載入進記憶體。

每載入一種class,JVM就為其建立一個Class型別的例項,並關聯起來。注意:這裡的Class型別是一個名叫Class的class。它長這樣:

public final class Class {
    private Class() {}
}

以String類為例,當JVM載入String類時,它首先讀取String.class檔案到記憶體,然後,為String類建立一個Class例項並關聯起來:

Class cls = new Class(String);

這個Class例項是JVM內部建立的,如果我們檢視JDK原始碼,可以發現Class類的構造方法是private,只有JVM能建立Class例項,我們自己的Java程式是無法建立Class例項的。

所以,JVM持有的每個Class例項都指向一個數據型別(class或interface):

一個Class例項包含了該class的所有完整資訊:

由於JVM為每個載入的class建立了對應的Class例項,並在例項中儲存了該class的所有資訊,包括類名、包名、父類、實現的介面、所有方法、欄位等,因此,如果獲取了某個Class例項,我們就可以通過這個Class例項獲取到該例項對應的class的所有資訊。

這種通過Class例項獲取class資訊的方法稱為反射(Reflection)

如何獲取一個class的Class例項?

方法一:直接通過一個class的靜態變數class獲取:

Class cls = String.class;

方法二:如果我們有一個例項變數,可以通過該例項變數提供的getClass()方法獲取:

String s = "Hello";
Class cls = s.getClass();

方法三:如果知道一個class的完整類名,可以通過靜態方法Class.forName()獲取:

Class cls = Class.forName("java.lang.String");

因為Class例項在JVM中是唯一的,所以,上述方法獲取的Class例項是同一個例項。

注意一下Class例項比較和instanceof的差別:

Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true,因為n是Integer型別
boolean b2 = n instanceof Number; // true,因為n是Number型別的子類

boolean b3 = n.getClass() == Integer.class; // true,因為n.getClass()返回Integer.class
boolean b4 = n.getClass() == Number.class; // false,因為Integer.class!=Number.class

用instanceof不但匹配指定型別,還匹配指定型別的子類。而用==判斷class例項可以精確地判斷資料型別,但不能作子型別比較。

通常情況下,我們應該用instanceof判斷資料型別,因為面向抽象程式設計的時候,我們不關心具體的子型別。只有在需要精確判斷一個型別是不是某個class的時候,我們才使用==判斷class例項。

因為反射的目的是為了獲得某個例項的資訊。因此,當我們拿到某個Object例項時,我們可以通過反射獲取該Object的class資訊:

動態載入

JVM在執行Java程式的時候,並不是一次性把所有用到的class全部載入到記憶體,而是第一次需要用到class時才載入。例如:

// Main.java
public class Main {
    public static void main(String[] args) {
        if (args.length > 0) {
            create(args[0]);
        }
    }

    static void create(String name) {
        Person p = new Person(name);
    }
}

當執行Main.java時,由於用到了Main,因此,JVM首先會把Main.class載入到記憶體。然而,並不會載入Person.class,除非程式執行到create()方法,JVM發現需要載入Person類時,才會首次載入Person.class。如果沒有執行create()方法,那麼Person.class根本就不會被載入。

這就是JVM動態載入class的特性。

動態載入class的特性對於Java程式非常重要。利用JVM動態載入class的特性,我們才能在執行期根據條件載入不同的實現類。

訪問欄位

對任意的一個Object例項,只要我們獲取了它的Class,就可以獲取它的一切資訊。

我們先看看如何通過Class例項獲取欄位資訊。Class類提供了以下幾個方法來獲取欄位:

  • Field getField(name):根據欄位名獲取某個public的field(包括父類)

  • Field getDeclaredField(name):根據欄位名獲取當前類的某個field(不包括父類)

  • Field[] getFields():獲取所有public的field(包括父類)

  • Field[] getDeclaredFields():獲取當前類的所有field(不包括父類)

我們來看一下示例程式碼:

public class Main {
    public static void main(String[] args) throws Exception {
        Class stdClass = Student.class;
        // 獲取public欄位"score":
        System.out.println(stdClass.getField("score"));
        // 獲取繼承的public欄位"name":
        System.out.println(stdClass.getField("name"));
        // 獲取private欄位"grade":
        System.out.println(stdClass.getDeclaredField("grade"));
    }
}

class Student extends Person {
    public int score;
    private int grade;
}

class Person {
    public String name;
}

上述程式碼首先獲取Student的Class例項,然後,分別獲取public欄位、繼承的public欄位以及private欄位,打印出的Field類似:

public int Student.score
public java.lang.String Person.name
private int Student.grade

小結

  • Java的反射API提供的Field類封裝了欄位的所有資訊:

  • 通過Class例項的方法可以獲取Field例項:getField(),getFields(),getDeclaredField(),getDeclaredFields();

  • 通過Field例項可以獲取欄位資訊:getName(),getType(),getModifiers();

  • 通過Field例項可以讀取或設定某個物件的欄位,如果存在訪問限制,要首先呼叫setAccessible(true)來訪問非public欄位。

  • 通過反射讀寫欄位是一種非常規方法,它會破壞物件的封裝。

呼叫方法

Java的反射API提供的Method物件封裝了方法的所有資訊:

通過Class例項的方法可以獲取Method例項:

  • getMethod()

  • getMethods()

  • getDeclaredMethod()

  • getDeclaredMethods()

通過Method例項可以獲取方法資訊:

  • getName()

  • getReturnType()

  • getParameterTypes()

  • getModifiers()

通過Method例項可以呼叫某個物件的方法:Object invoke(Object instance, Object... parameters);

通過設定setAccessible(true)來訪問非public方法;

通過反射呼叫方法時,仍然遵循多型原則。

呼叫構造方法

Constructor物件封裝了構造方法的所有資訊;

通過Class例項的方法可以獲取Constructor例項:

  • getConstructor():獲取某個public的Constructor

  • getConstructors():獲取所有public的Constructor

  • getDeclaredConstructor():獲取某個Constructor

  • getDeclaredConstructors():獲取所有Constructor

通過Constructor例項可以建立一個例項物件:newInstance(Object... parameters);

通過設定setAccessible(true)來訪問非public構造方法。

獲取繼承關係

當我們獲取到某個Class物件時,實際上就獲取到了一個類的型別:

Class cls = String.class; // 獲取到String的Class

還可以用例項的getClass()方法獲取:

String s = "";
Class cls = s.getClass(); // s是String,因此獲取到String的Class

最後一種獲取Class的方法是通過Class.forName(""),傳入Class的完整類名獲取:

Class s = Class.forName("java.lang.String");

這三種方式獲取的Class例項都是同一個例項,因為JVM對每個載入的Class只建立一個Class例項來表示它的型別。

獲取父類的Class

有了Class例項,我們還可以獲取它的父類的Class:

public class Main {
    public static void main(String[] args) throws Exception {
        Class i = Integer.class;
        Class n = i.getSuperclass();
        System.out.println(n);
        Class o = n.getSuperclass();
        System.out.println(o);
        System.out.println(o.getSuperclass());
    }
}

輸出:

class java.lang.Number
class java.lang.Object
null

執行上述程式碼,可以看到,Integer的父類型別是Number,Number的父類是Object,Object的父類是null。

除Object外,其他任何非interface的Class都必定存在一個父類型別.

通過Class物件可以獲取繼承關係:

  • Class getSuperclass():獲取父類型別;

  • Class[] getInterfaces():獲取當前類實現的所有介面。

通過Class物件的isAssignableFrom()方法可以判斷一個向上轉型是否可以實現.

動態代理

Java標準庫提供了一種動態代理(Dynamic Proxy)的機制:可以在執行期動態建立某個interface的例項。

什麼叫執行期動態建立?聽起來好像很複雜。所謂動態代理,是和靜態相對應的。我們來看靜態程式碼怎麼寫:

定義介面:

public interface Hello {
    void morning(String name);
}

編寫實現類:

public class HelloWorld implements Hello {
    public void morning(String name) {
        System.out.println("Good morning, " + name);
    }
}

建立例項,轉型為介面並呼叫:

Hello hello = new HelloWorld();
hello.morning("Bob");

這種方式就是我們通常編寫程式碼的方式。

還有一種方式是動態程式碼,我們仍然先定義了介面Hello,但是我們並不去編寫實現類,而是直接通過JDK提供的一個Proxy.newProxyInstance()建立了一個Hello介面物件。

這種沒有實現類但是在執行期動態建立了一個介面物件的方式,我們稱為動態程式碼。

JDK提供的動態建立介面物件的方式,就叫動態代理。

在執行期動態建立一個interface例項的方法如下:

  • 定義一個InvocationHandler例項,它負責實現介面的方法呼叫;

  • 通過Proxy.newProxyInstance()建立interface例項,它需要3個引數:

  1. 使用的ClassLoader,通常就是介面類的ClassLoader;
  2. 需要實現的介面陣列,至少需要傳入一個介面進去;
  3. 用來處理介面方法呼叫的InvocationHandler例項。

將返回的Object強制轉型為介面。

每天學習一點點,每天進步一點點。