點擊藍色字免費訂閱,天天收到這樣的好信息java
單單是問反射有什麼用,其實最經常使用的就兩個:面試
-
根據類名建立實例(類名能夠從配置文件讀取,不用new,達到解耦)spring
-
用Method.invoke執行方法數據庫
可是這些其實不難理解,難的是反射自己。若是有興趣能夠往下看:vim
因爲反射自己確實抽象(說是Java中最抽象的概念也不爲過),因此我當初寫做時也用了大量的比喻。可是比喻有時會讓答案偏離得更遠。前陣子看了些講設計模式的文章,把比喻都用壞了。有時理解比喻,居然要比理解設計模式自己還費勁...那就南轅北轍了。因此,這一次,能不用比喻就儘可能不用,爭取用最實在的代碼去解釋。設計模式
主要內容:數組
-
JVM是如何構建一個實例的緩存
-
.class文件微信
-
類加載器mybatis
-
Class類
-
反射API
JVM是如何構建一個實例的
下文我會使用的名詞及其對應關係
-
內存:即JVM內存,棧、堆、方法區啥的都是JVM內存,只是人爲劃分
-
.class文件:就是所謂的字節碼文件,這裏稱.class文件,直觀些
假設main方法中有如下代碼:
Person p = new Person();
不少初學者會覺得整個建立對象的過程是下面這樣的
javac Person.java java Person
不能說錯,可是粗糙了一點。
稍微細緻一點的過程能夠是下面這樣的
經過new建立實例和反射建立實例,都繞不開Class對象。
.class文件
有人用編輯器打開.class文件看過嗎?
好比我如今寫一個類
用vim命令打開.class文件,以16進制顯示就是下面這副鬼樣子:
在計算機中,任何東西底層保存的形式都是0101代碼。
.java源碼是給人類讀的,而.class字節碼是給計算機讀的。根據不一樣的解讀規則,能夠產生不一樣的意思。就比如「這週日你有空嗎」,合適的斷句很重要。
一樣的,JVM對.class文件也有一套本身的讀取規則,不須要咱們操心。總之,0101代碼在它眼裏的樣子,和咱們眼中的英文源碼是同樣的。
類加載器
在最開始複習對象建立過程時,咱們瞭解到.class文件是由類加載器加載的。關於類加載器,若是掰開講,是有不少門道的,能夠看看
@請叫我程序猿大人
寫的好怕怕的類加載器。可是核心方法只有loadClass(),告訴它須要加載的類名,它會幫你加載:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,檢查是否已經加載該類 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { // 若是還沒有加載,則遵循父優先的等級加載機制(所謂雙親委派機制) if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // 模板方法模式:若是仍是沒有加載成功,調用findClass() long t1 = System.nanoTime(); c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } // 子類應該重寫該方法 protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
加載.class文件大體能夠分爲3個步驟:
-
檢查是否已經加載,有就直接返回,避免重複加載
-
當前緩存中確實沒有該類,那麼遵循父優先加載機制,加載.class文件
-
上面兩步都失敗了,調用findClass()方法加載
須要注意的是,ClassLoader類自己是抽象類,而抽象類是沒法經過new建立對象的。因此它的findClass()方法寫的很隨意,直接拋了異常,反正你沒法經過ClassLoader對象調用。也就是說,父類ClassLoader中的findClass()方法根本不會去加載.class文件。
正確的作法是,子類重寫覆蓋findClass(),在裏面寫自定義的加載邏輯。好比:
@Overridepublic Class<?> findClass(String name) throws ClassNotFoundException { try { /*本身另外寫一個getClassData() 經過IO流從指定位置讀取xxx.class文件獲得字節數組*/ byte[] datas = getClassData(name); if(datas == null) { throw new ClassNotFoundException("類沒有找到:" + name); } //調用類加載器自己的defineClass()方法,由字節碼獲得Class對象 return defineClass(name, datas, 0, datas.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException("類找不到:" + name); }}
defineClass()是ClassLoader定義的方法,目的是根據.class文件的字節數組byte[] b造出一個對應的Class對象。咱們沒法得知具體是如何實現的,由於最終它會調用一個native方法:
反正,目前咱們關於類加載只需知道如下信息:
Class類
如今,.class文件被類加載器加載到內存中,而且JVM根據其字節數組建立了對應的Class對象。因此,咱們來研究一下Class對象。
Class對象是Class類的實例,咱們將在這一小節一步步分析Class類的結構。
可是,在看源碼以前,我想問問聰明的各位,若是你是JDK源碼設計者,你會如何設計Class類?
假設如今有個BaseDto類
上面類至少包括如下信息(按順序):
-
權限修飾符
-
類名
-
參數化類型(泛型信息)
-
接口
-
註解
-
字段(重點)
-
構造器(重點)
-
方法(重點)
最終這些信息在.class文件中都會以0101表示:
整個.class文件最終都成爲字節數組byte[] b,裏面的構造器、方法等各個「組件」,其實也是字節。
因此,我猜Class類的字段至少是這樣的:
好了,看一下源碼是否是如我所料:

字段、方法、構造器對象

註解數據

泛型信息
等等。
並且,針對字段、方法、構造器,由於信息量太大了,JDK還單獨寫了三個類,好比Method類:
也就是說,Class類準備了不少字段用來表示一個.class文件的信息,對於字段、方法、構造器等,爲了更詳細地描述這些重要信息,還寫了三個類,每一個類裏面都有很詳細的對應。
也就是說,本來UserController類中全部信息,都被「解構」後保存在Class類、Method類等的字段中。
大概瞭解完Class類的字段後,咱們看看Class類的方法。
-
構造器
能夠發現,Class類的構造器是私有的,咱們沒法手動new一個Class對象,只能由JVM建立。JVM在構造Class對象時,須要傳入一個類加載器,而後纔有咱們上面分析的一連串加載、建立過程。
-
Class.forName()方法
反正仍是類加載器去搞唄。
-
newInstance()
也就是說,newInstance()底層就是調用無參構造對象的newInstance()。
因此,本質上Class對象要想建立實例,其實都是經過構造器對象。若是沒有空參構造對象,就沒法使用clazz.newInstance(),必需要獲取其餘有參的構造對象而後調用構造對象的newInstance()。
反射API
沒啥好說的,在平常開發中反射最終目的主要兩個:
-
建立實例
-
反射調用方法
建立實例的難點在於,不少人不知道clazz.newInstance()底層仍是調用Contructor對象的newInstance()。因此,要想調用clazz.newInstance(),必須保證編寫類的時候有個無參構造。
反射調用方法的難點,有兩個,初學者可能會不理解。
再此以前,先來理清楚Class、Field、Method、Constructor四個對象的關係:
Field、Method、Constructor對象內部有對字段、方法、構造器更詳細的描述:
OK,理清關係後咱們繼續來看看反射調用方法時的兩個難點。
-
難點一:爲何根據Class對象獲取Method時,須要傳入方法名+參數的Class類型
爲何要傳name和ParameterType?
由於.class文件中有多個方法,好比
因此必須傳入name,以方法名區分哪一個方法,獲得對應的Method。
那參數parameterTypes爲何要用Class類型,我想和調用方法時同樣直接傳變量名不行嗎,好比userName, age。
答案是:咱們沒法根據變量名區分方法
User getUser(String userName, int age); User getUser(String mingzi, int nianling);
這不叫重載,這就是同一個方法。只能根據參數類型。
我知道,你還會問:變量名不行,那我能不能傳String, int。
很差意思,這些都是基本類型和引用類型,類型不能用來傳遞。咱們能傳遞的要麼值,要麼對象(引用)。而String.class, int.class是對象,且是Class對象。
實際上,調用Class對象的getMethod()方法時,內部會循環遍歷全部Method,而後根據方法名和參數類型匹配惟一的Method返回。

循環遍歷全部Method,根據name和parameterType匹配
難點二:調用method.invoke(obj, args);時爲何要傳入一個目標對象?
上面分析過,.class文件經過IO被加載到內存後,JDK創造了至少四個對象:Class、Field、Method、Constructor,這些對象其實都是0101010的抽象表示。
以Method對象爲例,它究竟是什麼,怎麼來的?咱們上面已經分析過,Method對象有好多字段,好比name(方法名),returnType(返回值類型)等。也就是說咱們在.java文件中寫的方法,被「解構」之後存入了Method對象中。因此對象自己是一個方法的映射,一個方法對應一個Method對象。
我在專欄的另外一篇文章中講過,對象的本質就是用來存儲數據的。而方法做爲一種行爲描述,是全部對象共有的,不屬於某個對象獨有。好比現有兩個Person實例
Person p1 = new Person(); Person p2 = new Person();
對象 p1保存了"hst"和18,p2保存了"cxy"和20。可是不論是p1仍是p2,都會有changeUser(),可是每一個對象裏面寫一份太浪費。既然是共性行爲,能夠抽取出來,放在方法區共用。
但這又產生了一個棘手的問題,方法是共用的,JVM如何保證p1調用changeUser()時,changeUser()不會跑去把p2的數據改掉呢?
因此JVM設置了一種隱性機制,每次對象調用方法時,都會隱性傳遞當前調用該方法的對象參數,方法能夠根據這個對象參數知道當前調用本方法的是哪一個對象!
一樣的,在反射調用方法時,本質仍是但願方法處理數據,因此必須告訴它執行哪一個對象的數據。
因此,把Method理解爲方法執行指令吧,它更像是一個方法執行器,必須告訴它要執行的對象(數據)。
固然,若是是invoke一個靜態方法,不須要傳入具體的對象。由於靜態方法並不能處理對象中保存的數據。
打油詩
我不在意個人做品文章是被如今的人讀仍是由子孫後代來讀。既然上帝花了六千年來等一位觀察者,我能夠花上一個世紀來等待讀者。
往期推薦
本文分享自微信公衆號 - Java小白學心理(gh_9a909fa2fb55)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。