Java的基本使用之反射
1、反射的概念
反射就是Reflection,Java的反射是指程序在運行期可以拿到一個對象的所有信息。反射是為了解決在運行期,對某個實例一無所知的情況下,去調用其方法。
2、Class實例
除了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);
JVM持有的每個Class實例都指向一個數據類型(class或interface)。一個Class實例包含了該class的所有完整信息,包括類名、包名、父類、實現的接口、所有方法、字段等,因此,如果獲取了某個Class實例,我們就可以通過這個Class實例獲取到該實例對應的class的所有信息。這種通過Class實例獲取class信息的方法就稱為反射(Reflection)。
JVM也為每一種基本類型都創建了Class,如 int 的 Class可以通過int.class訪問。
2.1、如何獲取一個class的Class實例
1)直接通過一個class的靜態變量class獲取
Class cls = String.class;
2)如果有class的一個實例變量,可以直接通過 class 的實例變量提供的getClass()方法獲取
String s = "Hello";
Class cls = s.getClass();
3)如果知道class的完整類名,可以通過靜態方法Class.forName()獲取
Class cls = Class.forName("java.lang.String");
一種class的Class實例在JVM中是唯一的。
如果獲取到了一個Class實例,我們就可以通過該Class實例來創建對應類型的實例:
// 獲取String的Class實例: Class cls = String.class; // 創建一個String實例: String s = (String) cls.newInstance(); //創建了一個空字符串
上述代碼相當于new String()。通過Class.newInstance()可以創建類實例,它的局限是:只能調用public的無參數構造方法。帶參數的構造方法,或者非public的構造方法都無法通過Class.newInstance()被調用。
3、通過反射訪問類的字段
3.1、獲取類的字段信息
對任意的一個Object實例,只要我們獲取了它的Class,就可以獲取它的一切信息。我們可以通過Class實例獲取字段信息。
Class類提供了以下幾個方法來獲取字段對象:
- Field getField(name):根據字段名獲取某個public的field對象(包括父類)
- Field getDeclaredField(name):根據字段名獲取當前類的某個field對象(不包括父類,也包含非public)
- Field[] getFields():獲取所有public的field對象(包括父類)
- Field[] getDeclaredFields():獲取當前類的所有field對象(不包括父類,也包含非public)
代碼示例:
public class Main { public static void main(String[] args) throws Exception { Class stdClass = Student.class; // 獲取public字段"score": System.out.println(stdClass.getField("score")); //輸出public int Student.score // 獲取繼承的public字段"name": System.out.println(stdClass.getField("name")); //輸出public java.lang.String Person.name // 獲取private字段"grade": System.out.println(stdClass.getDeclaredField("grade")); //輸出private int Student.grade } } class Student extends Person { public int score; private int grade; } class Person { public String name; }
一個Field對象包含了一個字段的所有信息:
getName():返回字段名稱,例如,"name";getType():返回字段類型,也是一個Class實例,例如,String.class;getModifiers():返回字段的修飾符,它是一個整型int,不同的bit表示不同的含義??梢酝ㄟ^調用Modifier.isFinal(int n)、Modifier.isPublic(int n)、Modifier.isProtected(int n)、Modifier.isPrivate(int n)、Modifier.isStatic(int n)來判斷返回的修飾符
//以String類的value字段為例,它的定義如下: public final class String { private final byte[] value; } //通過反射獲取字段信息 Field f = String.class.getDeclaredField("value"); System.out.println(f.getName()); // "value" System.out.println(f.getType()); // class [B 表示byte[]類型 int m = f.getModifiers(); System.out.println(Modifier.isFinal(m)); //true System.out.println(Modifier.isPublic(m)); //false System.out.println(Modifier.isProtected(m)); //false System.out.println(Modifier.isPrivate(m)); //true System.out.println(Modifier.isStatic(m)); //false
3.2、獲取類的實例的字段值
我們可以通過反射拿到一個類實例對應的該字段的值。
例如,下面對于一個Person實例,我們先拿到name字段對應的Field,再獲取這個實例的name字段的值:
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(); //獲取Class實例 Field f = c.getDeclaredField("name"); //獲取Field實例 f.setAccessible(true); //調用Field.setAccessible(true)使其可以訪問private字段,否則會報錯。也可以將 Person 的 name 字段改成public Object value = f.get(p); //用Field.get(Object)獲取指定實例的指定字段的值。 System.out.println(value); // "Xiao Ming" } } class Person { private String name; public Person(String name) { this.name = name; } }
使用反射可以獲取private字段的值,但是反射是一種非常規的用法,使用反射,首先代碼非常繁瑣,其次,它更多地是給工具或者底層框架來使用,目的是在不知道目標實例任何信息的情況下,獲取特定字段的值。所以說類的封裝還是有意義的。
setAccessible(true)可能會失敗。如果JVM運行期存在SecurityManager,那么它會根據規則進行檢查,有可能阻止setAccessible(true)。例如,某個SecurityManager可能不允許對java和javax開頭的package的類調用setAccessible(true),這樣可以保證JVM核心庫的安全。
3.3、設置實例的字段值
我們可以通過Field.set(Object, Object)來設置實例的字段值,其中第一個Object參數是指定的實例,第二個Object參數是待修改的值。
下面代碼通過反射修改實例的name字段值:
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); //修改非public字段,需要先調用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; } }
4、通過反射調用方法
4.1、通過Class實例獲取方法
我們可以通過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 int Student.getScore(java.lang.String)
// 獲取繼承的public方法getName,無參數: System.out.println(stdClass.getMethod("getName")); //輸出public java.lang.String Person.getName()
// 獲取private方法getGrade,參數為int: System.out.println(stdClass.getDeclaredMethod("getGrade", int.class)); //輸出private int Student.getGrade(int) } } 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"; } }
一個Method對象包含一個方法的所有信息:
getName():返回方法名稱,例如:"getScore";getReturnType():返回方法返回值類型,也是一個Class實例,例如:String.class;getParameterTypes():返回方法的參數類型,是一個Class數組,例如:{String.class, int.class};getModifiers():返回方法的修飾符,它是一個int,不同的bit表示不同的含義。
4.2、調用方法
對Method實例調用invoke就相當于調用該方法。invoke() 方法的返回值是一個 Object 對象,invoke() 的第一個參數是對象實例,即在哪個實例上調用該方法,后面的可變參數要與方法參數一致,否則將報錯。如果方法沒有參數,那么invoke也沒有第二個參數。
public class Main { public static void main(String[] args) throws Exception { //下面相當于是操作了 String r = s.substring(6); // "world" String s = "Hello world"; Method m = String.class.getMethod("substring", int.class); // 獲取String substring(int)方法,參數為int: String r = (String) m.invoke(s, 6); // 在s對象上調用該方法并獲取結果 System.out.println(r); } }
4.2.1、調用靜態方法
如果獲取到的Method表示一個靜態方法,調用靜態方法時,由于無需指定實例對象,所以invoke方法傳入的第一個參數永遠為null。我們以Integer.parseInt(String)為例:
import java.lang.reflect.Method; 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); } }
4.2.2、調用非public方法
和Field類似,對于非public方法,我們雖然可以通過Class.getDeclaredMethod()獲取該方法實例,但直接對其調用將得到一個IllegalAccessException。為了調用非public方法,我們通過Method.setAccessible(true)允許其調用:
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核心庫的安全。
4.3、多態
使用反射調用方法時,仍然遵循多態原則:即總是調用實際類型的覆寫方法(如果存在)。
如下:Person類定義了hello()方法,并且它的子類Student也覆寫了hello()方法,從Person.class獲取的Method,作用于Student實例時,調用的是Student類的方法
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()); //輸出Student:hello } } class Person { public void hello() { System.out.println("Person:hello"); } } class Student extends Person { public void hello() { System.out.println("Student:hello"); } }
5、通過反射調用構造方法創建類實例
我們可以通過 Class 提供的 newInstance() 方法來創建類的實例。但是 Class.newInstance() 只能調用該類的 public 無參數構造方法。如果構造方法帶有參數,或者不是public,就無法直接通過Class.newInstance()來調用。
Person p = Person.class.newInstance(); //Class.newInstance()返回Object對象
為了調用任意的構造方法,Java的反射API提供了Constructor對象,它包含一個構造方法的所有信息,可以創建一個實例。
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,可獲取不是public修飾的構造函數,比如private修飾的;getConstructors():獲取所有public的Constructor;getDeclaredConstructors():獲取所有的Constructor,不只是public修飾的。
調用非public的Constructor時,必須首先通過 Class.setAccessible(true)設置允許訪問,才能調用 Class.newInstance() 方法來創建實例,否則會報錯。不過setAccessible(true)也可能會失敗。
6、通過反射獲取繼承關系
6.1、獲取父類的Class實例
通過Class實例,我們可以獲取該實例 Class 實例的類的父類的 Class 實例:
public class Main { public static void main(String[] args) throws Exception { Class i = Integer.class; Class n = i.getSuperclass(); //class java.lang.Number System.out.println(n); Class o = n.getSuperclass(); //class java.lang.Object System.out.println(o); System.out.println(o.getSuperclass()); //null } }
運行上述代碼,可以看到,Integer的父類類型是Number,Number的父類是Object,Object的父類是null。除Object外,其他任何非interface的Class都必定存在一個父類類型。
對所有interface的Class調用getSuperclass()返回的是null,獲取接口的父接口要用getInterfaces()
6.2、獲取該類實現的接口interface
由于一個類可能實現一個或多個接口,通過Class的getInterfaces()方法我們就可以查詢到實現的接口類型。如果一個類沒有實現任何interface,那么getInterfaces()返回空數組。
例如,查詢Integer實現的接口:
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); //interface java.lang.Comparable interface java.lang.constant.Constable interface java.lang.constant.ConstantDesc } } }
getInterfaces()只返回當前類直接實現的接口類型,并不包括其父類實現的接口類型。
6.3、根據Class實例判斷類是否可向上轉型(isAssignableFrom())
根據兩個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
7、動態加載
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的特性,我們才能在運行期根據條件加載不同的實現類。
8、動態代理
運用Java的動態代理,可以在每個類中的執行方法前后進行一些統一的操作,由此可以不必修改每個類中的每個方法。
比如下面: 我們先寫一個被代理對象的接口和實現類。接口是Bird (鴨子是鳥類的一種),被代理的實現類是鴨子Duck
//接口 public interface Bird { String name="bird"; public void say(); } //實現類 public class Duck implements Bird { @Override public void say() { System.out.println("鴨子。。。"); } }
再寫一個鴨子的代理類。需要注意4點:
a. 代理類需實現 InvocationHandler 接口。
b.代理類有一個Object的屬性,通過構造器注入被代理對象。
c. 在代理類里面的invoke方法進行對被代理對象的調用,及調用前后的額外處理(你想在調用前后干啥就干啥唄)。
d.寫一個獲取代理對象的get方法,方便獲取代理對象。這個代理對象執行被代理對象的方法時,會執行invoke方法。
public class DuckProxy implements InvocationHandler { private Object duckObject; public DuckProxy(Object duckObject) { this.duckObject = duckObject; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //在代理真實對象前我們可以添加一些自己的操作 System.out.println("方法執行之前的操作"); System.out.println("Method:" + method); Object returnValue = method.invoke(duckObject, args); //執行增強后的方法,returnValue即為真實對象執行方法的返回值。當通過代理對象來調用真實對象的方法時,會自動地跳轉到代理對象關聯的handler對象的invoke方法來進行調用 //在代理真實對象后我們也可以添加一些自己的操作 System.out.println("方法執行之后的操作"); return returnValue; } public Object getDuckProxy(){ return Proxy.newProxyInstance(duckObject.getClass().getClassLoader(), duckObject.getClass().getInterfaces(), this); } }
測試運行:
//依次輸出: 方法執行之前的操作 Method:public abstract void proxyTest.Bird.say() 鴨子。。。 方法執行之后的操作 public class Test{ public static void main(String[] args){ Bird bird = new Duck(); Bird proxy = (Bird)new DuckProxy(bird).getDuckProxy(); //拿到代理對象 proxy.say(); } }
8.1、動態代理直接在運行期創建接口實例
我們可以通過Java標準庫提供的動態代理(Dynamic Proxy)的機制,在運行期動態創建某個接口interface的實例。這樣就可以不用編寫接口的實現類,直接在運行期創建接口的實例然后調用接口的方法。
如下:我們仍然先定義了接口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() { //定義一個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( //創建interface實例 Hello.class.getClassLoader(), // 傳入ClassLoader new Class[] { Hello.class }, // 傳入要實現的接口 handler // 傳入處理調用方法的InvocationHandler ); hello.morning("Bob"); hello.say(); } } interface Hello { void morning(String name); void say(); }
在運行期動態創建一個interface實例的方法如下:
- 先定義一個
InvocationHandler實例,它負責實現接口的方法調用; - 通過
Proxy.newProxyInstance()創建interface實例,它需要3個參數:- 使用的
ClassLoader,通常就是接口類的ClassLoader; - 需要實現的接口數組,至少需要傳入一個接口進去;
- 用來處理接口方法調用的
InvocationHandler實例。
- 使用的
- 將Proxy.newProxyInstance()返回的
Object強制轉型為接口。
動態代理實際上是JDK在運行期動態創建class字節碼并加載的過程,其實就是JDK幫我們自動編寫了一個類(不需要源碼,可以直接生成字節碼),并不存在可以直接實例化接口的黑魔法。

浙公網安備 33010602011771號