注:本文參考自:https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512
什麼是反射?
反射就是Reflection,Java的反射是指程序在運行期可以拿到一個對象的所有信息。
正常情況下,如果我們要調用一個對象的方法,或者訪問一個對象的字段,通常會傳入對象實例。反射是爲了解決在運行期,對某個實例一無所知的情況下,如何調用其方法。
1.Class類
1.1.Class類和反射(reflection)基本概念
除了int等基本類型外,Java的其他類型全部都是class(包括interface)。例如:
String
Object
Runnable
Exception
而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)。
1.2.三種方法獲取一個class的Class實例
1.2.1.方法1:直接通過一個class的靜態變量class獲取:
Class cls = String.class;
1.2.2.方法2:實例變量的getClass()方法
如果我們有一個實例變量,可以通過該實例變量提供的getClass()方法獲取:
String s = "Hello";
Class cls = s.getClass();
1.2.3.方法3:靜態方法Class.forName()獲取
如果知道一個class的完整類名,可以通過靜態方法Class.forName()獲取:
Class cls = Class.forName("java.lang.String");
1.3.Class實例比較
1.3.1.利用"=="比較兩個Class實例
因爲Class實例在JVM中是唯一的,所以,上述方法獲取的Class實例是同一個實例。可以用==比較兩個Class實例:
Class cls1 = String.class;
String s = "Hello";
Class cls2 = s.getClass();
boolean sameClass = cls1 == cls2; // true
1.3.2.與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實例。
1.4.數組也是一種Class
注意到數組(例如String[])也是一種Class,而且不同於String.class,它的類名是[Ljava.lang.String。此外,JVM爲每一種基本類型如int也創建了Class,通過int.class訪問。
如果獲取到了一個Class實例,我們就可以通過該Class實例來創建對應類型的實例:
// 獲取String的Class實例:
Class cls = String.class;
// 創建一個String實例:
String s = (String) cls.newInstance(); // 相當於 new String()
上述代碼相當於new String()。通過Class.newInstance()可以創建類實例,它的侷限是:只能調用【public && 無參數】構造方法。帶參數的構造方法,或者非public的構造方法都無法通過Class.newInstance()被調用。
1.5.動態加載
JVM在執行Java程序的時候,並不是一次性把所有用到的class全部加載到內存,而是第一次需要用到class時才加載。
2.訪問字段
2.1.Class類中訪問字段
對任意的一個Object實例,只要我們獲取了它的Class,就可以獲取它的一切信息。
我們先看看如何通過Class實例獲取字段信息。Class類提供了以下幾個方法來獲取字段:
- Field getField(name):根據字段名獲取某個public的field(包括父類)
- Field getDeclaredField(name):根據字段名獲取當前類的某個field(不包括父類)
- Field[] getFields():獲取所有public的field(包括父類)
- Field[] getDeclaredFields():獲取當前類的所有field(不包括父類)
一個Field對象包含了一個字段的所有信息:
- getName():返回字段名稱,類型是String,例如,“name”;
- getType():返回字段類型,也是一個Class實例,例如,String.class;
- getModifiers():返回字段的修飾符,它是一個int,不同的bit表示不同的含義。
以String類的value字段爲例,它的定義是:
public final class String {
private final byte[] value;
}
我們用反射獲取該字段的信息,代碼如下:
Field f = String.class.getDeclaredField("value");
f.getName(); // "value"
f.getType(); // class [B 表示byte[]類型
int m = f.getModifiers();
Modifier.isFinal(m); // true
Modifier.isPublic(m); // false
Modifier.isProtected(m); // false
Modifier.isPrivate(m); // true
Modifier.isStatic(m); // false
2.2.獲取字段值
利用反射拿到字段的一個Field實例只是第一步,我們還可以拿到一個實例對應的該字段的值。
例如,對於一個Person實例,我們可以先拿到name字段對應的Field,再獲取這個實例的name字段的值:
// reflection
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
Object p = new Person("Xiao Ming");
Class c = p.getClass();
Field f = c.getDeclaredField("name");
Object value = f.get(p);
System.out.println(value); // "Xiao Ming"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
}
上述代碼先獲取Class實例,再獲取Field實例,然後,用Field.get(Object)獲取指定實例的指定字段的值。
運行代碼,如果不出意外,會得到一個IllegalAccessException,這是因爲name被定義爲一個private字段,正常情況下,Main類無法訪問Person類的private字段。要修復錯誤,可以將private改爲public,或者,在調用Object value = f.get§;前,先寫一句:
f.setAccessible(true);
調用Field.setAccessible(true)的意思是,別管這個字段是不是public,一律允許訪問。
可以試着加上上述語句,再運行代碼,就可以打印出private字段的值。
2.3.設置字段值
通過Field實例既然可以獲取到指定實例的字段值,自然也可以設置字段的值。
設置字段值是通過Field.set(Object, Object)實現的,其中第一個Object參數是指定的實例,第二個Object參數是待修改的值。示例代碼如下:
// reflection
import java.lang.reflect.Field;
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person("Xiao Ming");
System.out.println(p.getName()); // "Xiao Ming"
Class c = p.getClass();
Field f = c.getDeclaredField("name");
f.setAccessible(true);
f.set(p, "Xiao Hong");
System.out.println(p.getName()); // "Xiao Hong"
}
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
運行上述代碼,打印的name字段從Xiao Ming變成了Xiao Hong,說明通過反射可以直接修改字段的值。
同樣的,修改非public字段,需要首先調用setAccessible(true)。
3.調用方法
3.1.Class類中獲取Method
我們已經能通過Class實例獲取所有Field對象,同樣的,可以通過Class實例獲取所有Method信息。Class類提供了以下幾個方法來獲取Method:
- Method getMethod(name, Class…):獲取某個public的Method(包括父類) Method
- getDeclaredMethod(name, Class…):獲取當前類的某個Method(不包括父類) Method[]
- getMethods():獲取所有public的Method(包括父類) Method[]
- getDeclaredMethods():獲取當前類的所有Method(不包括父類)
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass = Student.class;
// 獲取public方法getScore,參數爲String:
System.out.println(stdClass.getMethod("getScore", String.class));
// 獲取繼承的public方法getName,無參數:
System.out.println(stdClass.getMethod("getName"));
// 獲取private方法getGrade,參數爲int:
System.out.println(stdClass.getDeclaredMethod("getGrade", int.class));
}
}
class Student extends Person {
public int getScore(String type) {
return 99;
}
private int getGrade(int year) {
return 1;
}
}
class Person {
public String getName() {
return "Person";
}
}
上述代碼首先獲取Student的Class實例,然後,分別獲取public方法、繼承的public方法以及private方法,打印出的Method類似:
public int Student.getScore(java.lang.String)
public java.lang.String Person.getName()
private int Student.getGrade(int)
一個Method對象包含一個方法的所有信息:
- getName():返回方法名稱,例如:“getScore”;
- getReturnType():返回方法返回值類型,也是一個Class實例,例如:String.class;
- getParameterTypes():返回方法的參數類型,是一個Class數組,例如:{String.class, int.class};
- getModifiers():返回方法的修飾符,它是一個int,不同的bit表示不同的含義。
3.2.調用方法(通過反射執行Method)
當我們獲取到一個Method對象時,就可以對它進行調用。我們以下面的代碼爲例:
String s = "Hello world";
String r = s.substring(6); // "world"
如果用反射來調用substring方法,需要以下代碼:
public class Main {
public static void main(String[] args) throws Exception {
// String對象:
String s = "Hello world";
// 獲取String substring(int)方法,參數爲int:
Method m = String.class.getMethod("substring", int.class);
// 在s對象上調用該方法並獲取結果:
String r = (String) m.invoke(s, 6);
// 打印調用結果:
System.out.println(r);
}
}
注意到substring()有兩個重載方法,我們獲取的是String substring(int)這個方法。思考一下如何獲取String substring(int, int)方法。
Method m = String.class.getMethod("substring", int.class, int.class);
// 在s對象上調用該方法並獲取結果:
String r = (String) m.invoke(s, 4, 6);
對Method實例調用invoke就相當於調用該方法,invoke的第一個參數是對象實例,即在哪個實例上調用該方法,後面的可變參數要與方法參數一致,否則將報錯。
3.3.調用靜態方法
如果獲取到的Method表示一個靜態方法,調用靜態方法時,由於無需指定實例對象,所以invoke方法傳入的第一個參數永遠爲null。我們以Integer.parseInt(String)爲例:
public class Main {
public static void main(String[] args) throws Exception {
// 獲取Integer.parseInt(String)方法,參數爲String:
Method m = Integer.class.getMethod("parseInt", String.class);
// 調用該靜態方法並獲取結果:
Integer n = (Integer) m.invoke(null, "12345");
// 打印調用結果:
System.out.println(n);
}
}
3.4.調用非public方法
和Field類似,對於非public方法,我們雖然可以通過Class.getDeclaredMethod()獲取該方法實例,但直接對其調用將得到一個IllegalAccessException。爲了調用非public方法,我們通過Method.setAccessible(true)允許其調用:
// reflection
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Person p = new Person();
Method m = p.getClass().getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(p, "Bob");
System.out.println(p.name);
}
}
class Person {
String name;
private void setName(String name) {
this.name = name;
}
}
此外,setAccessible(true)可能會失敗。如果JVM運行期存在SecurityManager,那麼它會根據規則進行檢查,有可能阻止setAccessible(true)。例如,某個SecurityManager可能不允許對java和javax開頭的package的類調用setAccessible(true),這樣可以保證JVM核心庫的安全。
3.5.多態
我們來考察這樣一種情況:一個Person類定義了hello()方法,並且它的子類Student也覆寫了hello()方法,那麼,從Person.class獲取的Method,作用於Student實例時,調用的方法到底是哪個?
// reflection
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
// 獲取Person的hello方法:
Method h = Person.class.getMethod("hello");
// 對Student實例調用hello方法:
h.invoke(new Student());
}
}
class Person {
public void hello() {
System.out.println("Person:hello");
}
}
class Student extends Person {
public void hello() {
System.out.println("Student:hello");
}
}
運行上述代碼,發現打印出的是Student:hello,因此,使用反射調用方法時,仍然遵循多態原則:即總是調用實際類型的覆寫方法(如果存在)。上述的反射代碼:
Method m = Person.class.getMethod("hello");
m.invoke(new Student());
實際上相當於:
Person p = new Student();
p.hello();
4.調用構造方法
此部分可結合《Java從入門到精通》第16章的內容一起看。
我們通常使用new操作符創建新的實例:
Person p = new Person();
如果通過反射來創建新的實例,可以調用Class提供的newInstance()方法:
Person p = Person.class.newInstance();
調用Class.newInstance()的侷限是,它只能調用該類的public無參數構造方法。如果構造方法帶有參數,或者不是public,就無法直接通過Class.newInstance()來調用。
爲了調用任意的構造方法,Java的反射API提供了Constructor對象,它包含一個構造方法的所有信息,可以創建一個實例。Constructor對象和Method非常類似,不同之處僅在於它是一個構造方法,並且,調用結果總是返回實例:
import java.lang.reflect.Constructor;
public class Main {
public static void main(String[] args) throws Exception {
// 獲取構造方法Integer(int):
Constructor cons1 = Integer.class.getConstructor(int.class);
// 調用構造方法:
Integer n1 = (Integer) cons1.newInstance(123);
System.out.println(n1);
// 獲取構造方法Integer(String)
Constructor cons2 = Integer.class.getConstructor(String.class);
Integer n2 = (Integer) cons2.newInstance("456");
System.out.println(n2);
}
}
通過Class實例獲取Constructor的方法如下:
- getConstructor(Class…):獲取某個public的Constructor;
- getDeclaredConstructor(Class…):獲取某個Constructor;
- getConstructors():獲取所有public的Constructor;
- getDeclaredConstructors():獲取所有Constructor。
注意Constructor總是當前類定義的構造方法,和父類無關,因此不存在多態的問題。
調用非public的Constructor時,必須首先通過setAccessible(true)設置允許訪問。setAccessible(true)可能會失敗。
5.獲取繼承關係
在1.2小節中總結了獲取一個類Class實例的3種方法。
5.1.獲取父類的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());
}
}
運行上述代碼,可以看到,Integer的父類類型是Number,Number的父類是Object,Object的父類是null。除Object外,其他任何非interface的Class都必定存在一個父類類型。
5.2.獲取interface
由於一個類可能實現一個或多個接口,通過Class我們就可以查詢到實現的接口類型。例如,查詢Integer實現的接口:
// reflection
import java.lang.reflect.Method;
public class Main {
public static void main(String[] args) throws Exception {
Class s = Integer.class;
Class[] is = s.getInterfaces();
for (Class i : is) {
System.out.println(i);
}
}
}
要特別注意:getInterfaces()只返回當前類直接實現的接口類型,並不包括其父類實現的接口類型。
此外,對所有interface的Class調用getSuperclass()返回的是null,獲取接口的父接口要用getInterfaces():
System.out.println(java.io.DataInputStream.class.getSuperclass()); // java.io.FilterInputStream,因爲DataInputStream繼承自FilterInputStream
System.out.println(java.io.Closeable.class.getSuperclass()); // null,對接口調用getSuperclass()總是返回null,獲取接口的父接口要用getInterfaces()
如果一個類沒有實現任何interface,那麼getInterfaces()返回空數組。
5.3.繼承關係
當我們判斷一個實例是否是某個類型時,正常情況下,使用instanceof操作符:
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true
如果是兩個Class實例,要判斷一個向上轉型是否成立,可以調用isAssignableFrom():
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因爲Integer可以賦值給Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因爲Integer可以賦值給Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因爲Integer可以賦值給Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因爲Number不能賦值給Integer
6.動態代理
6.1.靜態代碼寫法
有沒有可能不編寫實現類,直接在運行期創建某個interface的實例呢?
這是可能的,因爲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");
6.2.動態代理【未細看】
還有一種方式是動態代碼,我們仍然先定義了接口Hello,但是我們並不去編寫實現類,而是直接通過JDK提供的一個Proxy.newProxyInstance()創建了一個Hello接口對象。這種沒有實現類但是在運行期動態創建了一個接口對象的方式,我們稱爲動態代碼。JDK提供的動態創建接口對象的方式,就叫動態代理。
一個最簡單的動態代理實現如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method);
if (method.getName().equals("morning")) {
System.out.println("Good morning, " + args[0]);
}
return null;
}
};
Hello hello = (Hello) Proxy.newProxyInstance(
Hello.class.getClassLoader(), // 傳入ClassLoader
new Class[] { Hello.class }, // 傳入要實現的接口
handler); // 傳入處理調用方法的InvocationHandler
hello.morning("Bob");
}
}
interface Hello {
void morning(String name);
}
動態代理實際上是JDK在運行期動態創建class字節碼並加載的過程,它並沒有什麼黑魔法,把上面的動態代理改寫爲靜態實現類大概長這樣:
public class HelloDynamicProxy implements Hello {
InvocationHandler handler;
public HelloDynamicProxy(InvocationHandler handler) {
this.handler = handler;
}
public void morning(String name) {
handler.invoke(
this,
Hello.class.getMethod("morning"),
new Object[] { name });
}
}
其實就是JDK幫我們自動編寫了一個上述類(不需要源碼,可以直接生成字節碼),並不存在可以直接實例化接口的黑魔法。