我第一次據說反射這個概念是在《Java編程思想》中看到的,提及這書我有些憂傷,當時自學Java,沒有前輩指導,本身摸着石子過河,隨便網上搜一下入門書籍,居然清一色的推薦《Java編程思想》(當時大概2016年初,也許只是我當時知識辨別能力比較低的緣由),如今看來,該書確實不適合入門,比較適合有必定開發經驗的開發者。有點扯遠了,拉回來,拉回來。java
在《Java編程思想》中提到反射的時候,做者將其看作是Java的RTTI,RTTI即Run Time Type Infomation(運行時類型信息),但實際上RTTI但是說是特指C++的RTTI,Java是沒有這個概念的,也許只是做者考慮到C++讀者比較多的緣由吧。shell
RTTI是C++語言的核心機制,它容許程序在運行時動態的決定各個對象的類型,例如常用到的dynamic_cast,該語法能夠將某個對象在運行時動態的轉換成其餘任意類型,但以後是否會發生錯誤,就不歸它管了。編程
反射也不是Java語言獨有的概念,而是計算機科學的通用概念,在維基百科上有以下解釋:數組
在計算機科學中,反射是指計算機程序在運行時(Run time)能夠訪問、檢測和修改它自己狀態或行爲的一種能力。用比喻來講,反射就是程序在運行的時候可以「觀察」而且修改本身的行爲。框架
要注意術語「反射」和「內省「type introspection)的關係。內省(或稱「自省」)機制僅指程序在運行時對自身信息(稱爲元數據)的檢測;反射機制不只包括要能在運行時對程序自身信息進行檢測,還要求程序能進一步根據這些信息改變程序狀態或結構。spa
從上面描述來看,反射和RTTI確實很像,但我更傾向於將RTTI認爲是反射的子集,反射包含的範圍應該更廣,不只能夠動態的轉換類型,還能夠在運行時對對象的行爲(方法),狀態(字段)作訪問、修改等操做。code
之因此要談到RTTI,是由於若是僅僅經過《Java編程思想》瞭解反射,可能會對做者的意思理解不深入,會認爲反射等同於C++ 的RTTI。(我當初就是這樣)對象
具體到Java中的反射,能夠這樣解釋:Java反射機制讓咱們能夠在運行時訪問任何一個類的元信息,包括其接口,父類,字段,方法等。JDK還提供了API讓咱們能夠方便使用反射機制,這些API都在java.lang.reflect包下,這些API包括Method,Field,Array等。不誇張的說,熟悉反射真的能夠在Java世界裏「隨心所欲」,不少框架很是依賴反射技術,例如Spring,MyBaties等,Spring會在運行時獲取類、方法、字段上的註解信息,而後對其作對應的處理。接口
類對象即Class對象,全部的類都有一個Class對象引用,該引用指向方法區中對應類的類信息,該引用在虛擬機規範中是有規定的,因此不管哪一種虛擬機實現都必定會有這麼一個Class對象引用,甚至基本類型都會有。例如:ip
public class Main {
public static void main(String[] args) {
//直接使用類型名.class的方式獲取
Class<?> intClass = int.class;
Class<?> userClass = User.class;
User user = new User();
//使用對象引用.getClass()的方式獲取
Class<?> userClass2 = user.getClass();
//對於基本類型,名字就是類型名,例如int類型的name就是int
System.out.println(intClass.getName());
//對於類來講,名字是全限定類名
System.out.println(userClass.getName());
System.out.println(userClass2.getName());
//simpleName是將包的信息略掉,只有類名
System.out.println(userClass.getSimpleName());
}
}
複製代碼
上面代碼用兩種方式獲取類對象,一種是直接使用類型名.class,一種是使用對象引用調用getClass()方法,基本類型只能使用第一種方法,引用類型兩種方法均可以使用,拿到類對象的引用以後就能夠「隨心所欲」了!能夠獲取到該類有幾個方法,分別是什麼方法,其方法簽名是怎樣的等等信息,下面的代碼演示了反射的簡單使用:
Field類有各類對字段進行操做的API,而獲取Field對象則須要先獲取類對象,而後經過調用getDeclaredFields()或者getFields()來獲取Field數組,其中getDeclaredFields()方法會包括私有字段,而getFields()不包括,還能夠經過調用getField(String)或者getDeclaredField(String)方法來獲取指定名字的字段,若是找不到就會拋出NoSuchFieldException異常。下面的代碼演示瞭如何操做字段:
public class Main {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException {
User user = new User();
Class<?> userClass = user.getClass();
//獲取字段
Field[] fields = userClass.getDeclaredFields();
for (Field field : fields) {
System.out.println("field Type is " + field.getType().getName() + " ---- field name is " + field.getName());
}
System.out.println("before set field value : " + user.getId());
Field field = userClass.getDeclaredField("id");
field.setAccessible(true);
field.set(user, 314L);
System.out.println("after set filed value : " + user.getId());
}
}
複製代碼
代碼中先獲取了字段數組,該數組包含了該類聲明的全部字段,經過Field對象,咱們能夠獲取對象名字,類型,甚至該字段在某個對象中的值。以後經過getDeclaredField("id")獲取了名爲id的的字段,並設置其可訪問性爲true,若是該字段是私有字段,不設置訪問性爲true的話,將沒法訪問該字段,緊接着使用set方法設置該字段的值,set方法有兩個參數,第一個參數是要做用的對象實例,第二個參數是字段的值,下面是該程序運行的結果:
field Type is java.lang.Long ---- field name is id
field Type is java.lang.String ---- field name is username
field Type is java.lang.String ---- field name is password
before set field value : null
after set filed value : 314
複製代碼
除此以外,還能夠獲取字段的註解、其父類等信息,Spring 框架的IOC容器有自動裝配的功能,能夠自動對字段進行賦值,該功能的實現原理就是依賴反射,運行時獲取字段的類型信息,註解信息(用來判斷是否要進行自動裝配),而後在容器中查找該類的實例,查到就直接對其賦值,查不到就拋出異常。
Method類也有不少對方法進行操做的API,不過大多數都是獲取方法的信息,例如方法的返回值,參數列表,參數個數,方法名等,幾乎沒有修改方法的API。其API的命名和Filed的極爲類似,能夠說是用的同一種命名模式。下面的代碼演示瞭如何對方法進行操做:
public class Main {
public static void main(String[] args) throws IllegalAccessException, NoSuchFieldException, NoSuchMethodException, InvocationTargetException {
User user = new User();
Class<?> userClass = user.getClass();
//獲取方法
Method[] methods = userClass.getDeclaredMethods();
for (Method method : methods) {
System.out.println("method return type is " + method.getReturnType());
System.out.println("method name is " + method.getName());
System.out.println("method params count " + method.getParameterCount());
Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters) {
System.out.println("param type is " + parameter.getType());
System.out.println("param name is " + parameter.getName());
}
System.out.println("----------------------------------------");
}
Method testMethod1 = userClass.getMethod("testMethod1", int.class, int.class);
testMethod1.setAccessible(true);
testMethod1.invoke(user, 1,1); //調用該方法
}
}
複製代碼
和Filed同樣,先經過getDeclaredMethods()獲取全部方法,每一個方法都是一個Method對象實例,這只是JDK API對其進行的抽象,實際上在虛擬機中並無那麼簡單,而後經過各類API來獲取信息,在代碼中獲取了方法的返回值,名字,參數各類以及其參數列表,同時遍歷了其參數列表。最後經過getMethod()指定相關參數獲取了指定的方法對象實例,getMethod()的第一個參數是方法名,第二個參數是幾個可變參數,表示參數的類對象,我定義的testMethod1只有兩個int參數,因此這裏傳入了兩個int.class對象,若是沒有找到對應的方法,就拋出NoSuchMethodException異常。
隨後將其設置成可訪問的,並使用invoke調用該方法,invoke的第一個參數是要做用的對象實例,第二個參數也是一個可變參數,須要傳入的是參數的值。最後將程序運行,大體能夠看到以下輸出:
....
method type is void
method name is setPassword
method params count 1
param type is class java.lang.String
param name is arg0
----------------------------------------
method type is void
method name is testMethod1
method params count 2
param type is int
param name is arg0
param type is int
param name is arg1
----------------------------------------
....
複製代碼
其實還有更多,我這裏只是截取了部分。輸出大部份內容符合咱們預期,但參數名輸出的東西是什麼鬼?arg0、arg1是個什麼東西?
咱們一直在說反射是運行時的一種機制,即操做的對象是編譯後的字節碼,java8以前方法的參數名在編譯以後會被相似arg0,arg1代替,java8以後提供了一個-parameters 編譯選項,該選擇默認是關閉的,指定以後纔會打開,打開狀況下,編譯後的字節碼就會使用源碼的參數名稱了。那在此以前,有什麼辦法運行時獲取字段名稱呢?答案是使用ASM等字節碼技術,關於該技術的使用,本文不會涉及,有興趣的朋友能夠到網上搜索相關資料。
反射也是一項博大精深的技術,本文僅僅是簡單的介紹了反射的簡單使用,關於其更多的使用其實和操做字段、方法差很少,一通百通便可,實在不行再看看JDK文檔就確定會了。關於其原理,本文沒涉及,緣由是若是讀者對虛擬機有必定了解的話,不難猜到其原理,其實這些什麼字段、方法、註解、接口等信息在類加載完成以後會被存儲在方法區裏,同時還留了一個Class對象的引用用於訪問這些信息。
不少框架都或多或少的使用到反射,實在是由於反射很是適合作這種「幕後」的事。最後,學好反射真的能夠在Java世界裏「隨心所欲」。