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個引數:
- 使用的ClassLoader,通常就是介面類的ClassLoader;
- 需要實現的介面陣列,至少需要傳入一個介面進去;
- 用來處理介面方法呼叫的InvocationHandler例項。
將返回的Object強制轉型為介面。
每天學習一點點,每天進步一點點。