反射和多態的實現原理詳解以及區別
反射和多態
事實上,反射和多態這兩種技術并無直接聯系,之所以把它們放在一起說,是因為,在Java技術體系中所提供的能夠讓我們在運行時識別對象和類的類型信息的方式,一共有兩種:即反射和多態關鍵技術RTTI
RTTI,即run-Time Type Identification運行時類型判定,它的作用是在我們不知道某個對象的確切的類型信息時,即某個對象是哪個類的實例的時候,可以通過RTTI相關的機制幫助我們在編譯時獲取對象的類型信息,這其實也就是多態的實現基礎。
反射機制允許我們在運行時發現和使用類的信息。因此多態和反射兩者的最大的共同點在于,他們都是運行時獲取程序信息的技術。反射技術對于java世界的眾多框架以及特性都發揮著至關重要的作用,博主認為中文的基石一詞能夠準確形容反射技術的地位,理解反射技術對于學習java相關技術的背后原理非常重要,許多java世界里的特性的實現原理背后都離不開反射。
讓我們先來看一下相對簡單一點的多態的相關知識以及實現原理RTTI相關的知識。
多態
什么是多態
首先明確一點我們在這里只考慮運行時多態,而不考慮編譯時多態(方法重載)。因此下列多態默認都是指運行時多態。
多態是面向對象編程里面的概念,一個接口的多種不同的實現方式,即為多態。注意這里的接口,不應理解得太死板,比如在java里面,繼承一個類和實現一個接口本質上都是一種繼承行為,因此都應該理解為多態的體現。
在計算機的世界里,尤其是編程的世界里,多態體現在:只有在運行的時候才知道引用變量所指向的具體實例對象。且有三個必要的條件:
- 繼承
- 重寫/實現
- 父類引用指向子類對象
多態的概念來源于生活,生活中的很多現象都是多態的體現,例如打印機,打印功能可以打印黑白色也可以打印彩色。同一款汽車可以用2.0l排量也可以有1.0l的排量。
多態的技術帶來的一個重要影響是:由于一個借口可能有多個實現,而每個實現之間的大小,規模,是不一樣的。因此多態對內存的分配是有影響的,不同的實現會有不同的內存分配. 這一點與現實世界的多態例子相比就會非常有意思,第一,我們會發現軟件里的多態是動態的多態,而現實世界里的多態大部分是一個預先設定好的多態體現,現實里的多態更多的類似于編譯時多態,即方法重載,例如打印機的例子。
java里多態的具體用法
如上面我們提到的一樣多態通常有兩種實現方法:
- 子類繼承父類(extends)
- 類實現接口(implements)
核心之處就在于對父類方法的改寫或對接口方法的實現,以取得在運行時不同的執行效果。要使用多態,在聲明對象時就應該遵循一條法則:聲明的總是父類類型或接口類型,而創建的是實際類型.
以ArrayList為例子,要使用多態的特性,要按照如下方式定義
- List list = new ArrayList();
此外,在定義方法參數時也通常總是應該優先使用父類類型或接口類型,例如:
- public void test(List list);
這樣聲明最大的好處在于它的靈活性,假如某一天ArrayList無法滿足要求,我們希望用LinkedList來代替它,那么只需要在對象創建的地方把new ArrayList()改為new LinkedList即可,其它代碼一概不用改動。
多態的實現原理與RTTI
RTTI,即Run-Time Type Identification運行時類型認定,通過運行時類型信息程序能夠使用父類的指針或引用來檢查這些指針或引用所指的對象的實際派生類型,是多態實現的技術基礎。RTTI的功能主要是通過Class類文件實現的,更精確一點是通過Class類文件的方法表實現的。
Class類是"類的類"(class of classes)。如果說類是對象的抽象的話,那么Class類就是對類的抽象。Class對象就是用來創建一個類的所有的常規對象的。每個類都有一個Class對象,每當編寫好并且編譯了一個新的類,就會生成一個它對應的Class對象,被保存在一個與類同名的.class文件中。java虛擬機中的被稱為類加載器的子系統,就是專門拿來做生成這個類的Class對象的工作的。
每一個Class類的對象代表一個特定的類。請看如下代碼
- import java.lang.Class;
- public class Test {
- public static void main(String[] args) throws ClassNotFoundException {
- Cycle unicycle = new Unicycle("Unicycle");
- Cycle.ride(unicycle);
- Class c1 = unicycle.getClass();//獲取clas對象
- System.out.println(c1.getName());
- Cycle bicycle = new Bicycle("Bicycle");
- Cycle.ride(bicycle);
- Class c2 = Class.forName("basic.Bicycle");//獲取clas對象
- System.out.println(c2.getName());
- Cycle tricycle = new Tricycle("Tricycle");
- Cycle.ride(tricycle);
- Class c3 = Tricycle.class;//獲取clas對象
- System.out.println(c3.getName());
- }
- }
- //父類
- class Cycle {
- private String name;
- public Cycle(String str) {
- name = str;
- }
- public static void ride(Cycle c) {
- System.out.println(c.name + "is riding");
- }
- }
- class Unicycle extends Cycle {
- private String name;
- public Unicycle(String str) {
- super(str);
- name = str;
- }
- }
- class Bicycle extends Cycle {
- private String name;
- public Bicycle(String str) {
- super(str);
- name = str;
- }
- }
- class Tricycle extends Cycle {
- private String name;
- public Tricycle(String str) {
- super(str);
- name = str;
- }
- }
這是一個普通的多態的示例程序,但是我在每一處多態調用時,分別去獲取了他們的class對象并打印出來。打印結果如下:
- Unicycleis riding
- basic.Unicycle
- Bicycleis riding
- basic.Bicycle
- Tricycleis riding
- basic.Tricycle
可以發現即使我們將對象的引用向上轉型,對象所指向的Class類對象依然是實際的實現類。
Java中每個對象都有相應的Class類對象,因此,我們隨時能通過Class對象知道某個對象“真正”所屬的類。無論我們對引用進行怎樣的類型轉換,對象本身所對應的Class對象都是同一個。這意味著java在運行時的確能確定真正的實現類是哪一個。
下面從虛擬機運行時的角度來簡要介紹多態的實現原理,這里以Java虛擬機規范的實現為例。
在JVM執行Java字節碼時,類型信息被存放在方法區中,通常為了優化對象調用方法的速度,方法區的類型信息中增加一個指針,該指針指向一張記錄該類方法入口的表(稱為方法表),表中的每一項都是指向相應方法的指針。
方法表的構造如下:
由于Java的單繼承機制,一個類只能繼承一個父類,而所有的類又都繼承自Object類。方法表中最先存放的是Object類的方法,接下來是該類的父類的方法,最后是該類本身的方法。方法表從上至下如下圖所示
這里關鍵的地方在于,如果子類改寫了父類的方法,那么子類和父類的那些同名方法共享一個方法表項,都被認作是父類的方法。如下所示
注意這里只有非私有的實例方法才會出現,并且靜態方法也不會出現在這里,原因很容易理解:靜態方法跟對象無關,可以將方法地址直接引用,而不像實例方法需要間接引用。
更深入地講,靜態方法是由虛擬機指令invokestatic調用的,私有方法和構造函數則是由invokespecial指令調用,只有被invokevirtual和invokeinterface指令調用的方法才會在方法表中出現。
由于以上方法的排列特性(Object——父類——子類),使得方法表的偏移量總是固定的。例如,對于任何類來說,其方法表中equals方法的偏移量總是一個定值,所有繼承某父類的子類的方法表中,其父類所定義的方法的偏移量也總是一個定值。
前面說過,方法表中的表項都是指向該類對應方法的指針,這里就開始了多態的實現:
假設Class B是Class A的子類,并且B重寫了A的方法method(),那么在B的方法表中,method()方法的指針指向的就是B的method方法入口而非類A的同名方法入口,也就是說,在虛擬機編譯生成B的Class文件中的方法表時,就實現了多態,之后只需讓對應的指令調用即可。
而對于A來說,它的方法表中的method方法則會指向其自身的method方法而非其父類的(這在類加載器載入該類時已經保證,同時JVM會保證總是能從對象引用指向正確的類型信息)。
結合方法指針偏移量是固定的以及指針總是指向實際類的方法域,我們不難發現多態的機制就在這里:
在調用方法時,實際上必須首先完成實例方法的符號引用解析,結果是該符號引用被解析為方法表的偏移量。虛擬機通過對象引用得到方法區中類型信息的入口,查詢類的方法表,當將子類對象聲明為父類類型時,形式上調用的是父類方法,此時虛擬機會從實際類的方法表(雖然聲明的是父類,但是實際上這里的類型信息中存放的是子類的信息)中查找該方法名對應的指針(這里用“查找”實際上是不合適的,前面提到過,方法的偏移量是固定的,所以只需根據偏移量就能獲得指針),進而就能指向實際類的方法了。
我們的故事還沒有結束,事實上上面的過程僅僅是利用繼承實現多態的內部機制,多態的另外一種實現方式:實現接口相比而言就更加復雜,原因在于,Java的單繼承保證了類的線性關系,而接口可以同時實現多個,這樣光憑偏移量就很難準確獲得方法的指針。所以在JVM中,多態的實例方法調用實際上有兩種指令:
- invokevirtual指令用于調用聲明為類的方法;
- invokeinterface指令用于調用聲明為接口的方法。
當使用invokeinterface指令調用方法時,就不能采用固定偏移量的辦法,只能老老實實挨個找了(當然實際實現并不一定如此,JVM規范并沒有規定究竟如何實現這種查找,不同的JVM實現可以有不同的優化算法來提高搜索效率)。
我們不難看出,在性能上,調用接口引用的方法通常總是比調用類的引用的方法要慢。這也告訴我們,在類和接口之間優先選擇接口作為設計并不總是正確的,當然設計問題不在本文探討的范圍之內,但顯然具體問題具體分析仍然不失為更好的選擇。
這就是多態的原理。總結起來說就是兩點:
- 是方法表起了決定性作用,如果子類改寫了父類的方法,那么子類和父類的那些同名方法共享一個方法表項,都被認作是父類的方法,因此可以寫成父類引用指向子類對象的形式。
- 類和接口的多態實現不一樣,類的方法表可以使用固定偏移,但接口只能挨個找,原因是接口的實現不是確定唯一的。
關于RTTI技術,典型的應用除了多態這樣的類型轉換,以及根據類生成Class對象這兩種形式以外。還有一種常見的用法,就是關鍵字Instanceof。Instanceof的作用是返回一個布爾值,告訴我們一個對象是不是某個特定類的一個實例。例如:
- if(x instanceof Dog){
- }
即在判斷x對象是不是Dog類的一個實例。
思考題:為什么使用多態創建一個對象引用之后,有的方法引用中不能調用?
反射
RTTI的作用是,當我們不知道一個對象的確切的類型的時候,可以通過RTTI來獲取。但是這個功能存在一個限制:要通過RTTI獲取的類型信息必須是在編譯時已知的,這句話怎么理解,我們通過多態的例子來說明:
- Animal dog = new Dog();
上述代碼定義的引用是Animal類型,但是在編譯時期,虛擬機可通過我們后面的new的代碼獲取到這個Animal的真正類型,這就是編譯時已知,這里有個前提條件是代碼里面其實是要給虛擬機留下這樣的判斷信息的。這事實上是一個限制。
原因在于,我們在很多時候并不能在編譯時獲知某個對象所述的類。例如,我們有個api的功能是接收字段并解析它屬于哪個類,但是傳入的字段可能是我們定義好的多個類的其中一個,這個時候就沒辦法在編譯時知道類型信息了。而只能在運行時通過傳入的字段進行判斷才有可能知道。
什么是反射
反射的定義如下:java程序在運行狀態中,對于任意一個類,都能夠在運行時知道這個類的所有屬性和方法;對于任意一個對象,都能夠調用它的任意方法和屬性;這種動態獲取信息以及動態調用對象方法的功能稱為java語言的反射機制。
上面這段話需要非常仔細的理解,我想通過問問題的形式來幫助讀者理解反射的概念。
問題1:什么是類的屬性和方法?
這個問題比較愚蠢,甚至有侮辱人的智商之嫌,但是博主的本意肯定不是這樣。這是一個很簡單問題,假設我們有下面代碼:
- public class Test{
- private String s;
- public void doNothing(){
- }
- }
那么,字符串s就是它的屬性,doNothing就是它的方法
問題2:什么叫一個類的所有屬性和方法
答:所有這個類中定義的成員變量,以及所有屬于這個類的方法。
問題3:問題2中的所有屬性和方法包含私有的嗎?
答案是包含的
問題4:任何一個類,除了屬性和方法,還有別的內容嗎?
答案是沒有了。注意,內部類不屬于外部類,本質上是兩個類。
問題5:什么叫獲取一個方法,什么叫獲取一個屬性?
這是一個很好的問題,理解了這個問題之后也就不會覺得反射過于抽象了。java當中提供了專門的方法的抽象Method類和專門的屬性的抽象Field類,以及專門的所有類的抽象Class類,并提供了一系列的方法,來幫助我們獲取一個類的屬性和方法。
獲取到的屬性和方法,將以普通對象的方式存在。與我們自己寫的類并無任何區別。下面是一個代碼例子:
- public class MethodClass {
- public static void main(String[] args) throws Exception {
- //1.獲取Class對象
- Class xxxClass = Class.forName("com.XXX.XXX");
- //2.獲取所有公有方法
- Method[] methodArray = xxxClass .getMethods();
- //3.獲取字段
- Field[] fieldArray = xxxClass .getFields();
- }
- }
反射的實現原理
Class類與java.lang.reflect庫一起對反射的概念提供了技術支持。java.lang.reflect類庫包含了Field類,Method類以及Constructor類。這些類用來表示未知類里對應的成員。Class類提供了獲取getFields()、getMethods()和getConstructors()等方法,而這些方法的返回值類型就定義在java.lang.reflect當中。
如果不知道某個對象的確切類型(即list引用到底是ArrayList類型還是LinkedList類型),RTTI可以告訴你,但是有一個前提:這個類型在編譯時必須已知,這樣才能使用RTTI來識別它。
要想理解反射的原理,必須要結合類加載機。反射機制并沒有什么神奇之處,當通過反射與一個未知類型的對象打交道時,JVM只是簡單地檢查這個對象,看它屬于哪個特定的類,然后再通過拿到的某一個類的全限定名去找這個類的Class文件 。因此,那個類的.class對于JVM來說必須是可獲取的,要么在本地機器上,要么從網絡獲取。所以對于RTTI和反射之間的真正區別只在于:
- RTTI,編譯器在編譯時打開和檢查.class文件
- 反射,運行時打開和檢查.class文件
對于反射機制而言.class文件在編譯時是不可獲取的,所以是在運行時獲取和檢查.class文件。
總結起來說就是,反射是通過Class類和java.lang.reflect類庫一起支持而實現的,其中每一個Class類的對象都對應了一個類,這些信息在編譯時期就已經被存在了.class文件里面了,Class 對象是在加載類時由 Java 虛擬機以及通過調用類加載器中的defineClass方法自動構造的。對于我們定義的每一個類,在虛擬機中都有一個應的Class對象。
那么在運行時期,無論是通過字面量還是forName方法獲取Class對象,都是去根據這個類的全限定名(全限定名必須是唯一的,這也間接回答了為什么類名不能重復這個問題。)然后獲取對應的Class對象
總結: java虛擬機幫我們生成了類的class對象,而通過類的全限定名,我們可以去獲取這個類的字節碼.class文件,然后再獲取這個類對應的class對象,再通過class對象提供的方法結合類Method,Filed,Constructor,就能獲取到這個類的所有相關信息. 獲取到這些信息之后,就可以使用Constructor創建對象,用get和set方法讀取和修改與Field對象相關的字段,用invoke方法調用與Method對象關聯的方法。
反射的應用
反射機制非常重要,應用也非常之廣泛。在使用反射時,我們的代碼里面可以出現任何一個具體的構造器,字段信息,方法,但是卻能動態的生成對象,調用他們的方法,這是一個非常通用的功能,由此帶來的價值也是驚人的。反射比較出名的應用有:
- Spring/Mybatis等框架,行內有一句這樣的老話:反射機制是Java框架的基石。最經典的就是xml的配置模式。
- JDBC 的數據庫的連接
- 動態生成對象,應用于工廠模式中. spring的bean容器也就是一個工廠
- jdk動態代理,利用反射獲取傳入接口的實現類
- 注解機制的實現,利用反射可以獲取每一個filed,Filed類提供了getDeclaredAnnotations方法以數組形式返回這個字段所有的注解....
- 編輯器代碼自動提示的實現
反射的弊端
1.性能
反射包括了一些動態類型,所以 JVM 無法對這些代碼進行優化。因此,反射操作的效率要比那些非反射操作低得多。我們應該避免在經常被 執行的代碼或對性能要求很高的程序中使用反射。
2.安全
使用反射技術要求程序必須在一個沒有安全限制的環境中運行。如果一個程序必須在有安全限制的環境中運行,如 Applet,那么這就是個問題了。
3.內部暴露
由于反射允許代碼執行一些在正常情況下不被允許的操作(比如訪問私有的屬性和方法),所以使用反射可能會導致意料之外的副作用--代碼有功能上的錯誤,降低可移植性。反射代碼破壞了抽象性,因此當平臺發生改變的時候,代碼的行為就有可能也隨著變化。
4.喪失了編譯時類型檢查的好處,包括異常檢查。如果程序企圖用反射去調用不存在或者不可訪問方法,在運行時將會失敗。
5.從代碼規范的角度來說,執行反射訪問所需要的代碼非常笨拙和冗長。這樣的代碼閱讀起來很困難
核心反射機制最初是為了基于組件的應用創建工具而設計的,如spring。這類工具通常需要裝載類,并且用反射功能找出它們支持哪些方法和構造器。這些工具允許用戶交互式的構建訪問這些類的應用程序。
反射功能只是在應用程序設計階段被用到,通常,普通應用程序運行時不應該以反射方式訪問對象。對于特定的復雜系統編程任務,它也許是非常必要的,但它也有一些缺點,如果你編寫的程序必須要與編譯時未知的類一起工作,如有可能就使用反射機制來實例化對象,而訪問對象時則使用編譯時已知的某個接口或者超類。
基于此,在effective java也總結了一條接口優先于反射機制的開發原則。
反射相關類
Class類
關于Class類請參考博客:[面向對象] 類與對象與Java里的Class類解析
Field類
即字段類,我們可以通過一個類的Class對象獲取其Field類的對象,然后java當中提供了這個Field類來提供反射獲取字段的相關信息,以及進行一些操作,比如set一個字段的值等功能。
Method類
即方法類,我們可以通過一個類的Class對象獲取其Method類的一個實例對象,并且使用獲得的Method對象去獲取這個方法的相關信息,以及調用這個方法的功能。
以上三個類建議讀者直接查看源代碼,看看他們提供的方法,就會理解得更加清楚。
反射應用實例
下面的代碼是使用反射獲取方法的例子程序
model類
- package com.dr.Reflection.getMethodByReflect;
- public class Student {
- //**************成員方法***************//
- public void show1(String s) {
- System.out.println("調用了:公有的,String參數的show1(): s = " + s);
- }
- protected void show2() {
- System.out.println("調用了:受保護的,無參的show2()");
- }
- void show3() {
- System.out.println("調用了:默認的,無參的show3()");
- }
- private String show4(int age) {
- System.out.println("調用了,私有的,并且有返回值的,int參數的show4(): age = " + age);
- return "abcd";
- }
- }
測試類
- package com.dr.Reflection.getMethodByReflect;
- import java.lang.reflect.Method;
- /*
- * 獲取成員方法并調用:
- *
- * 1.批量的:
- * public Method[] getMethods():獲取所有"公有方法";(包含了父類的方法也包含Object類)
- * public Method[] getDeclaredMethods():獲取所有的成員方法,包括私有的(不包括繼承的)
- * 2.獲取單個的:
- * public Method getMethod(String name,Class<?>... parameterTypes):
- * 參數:
- * name : 方法名;
- * Class ... : 形參的Class類型對象
- * public Method getDeclaredMethod(String name,Class<?>... parameterTypes)
- *
- * 調用方法:
- * Method --> public Object invoke(Object obj,Object... args):
- * 參數說明:
- * obj : 要調用方法的對象;
- * args:調用方式時所傳遞的實參;
- ):
- */
- public class MethodClass {
- public static void main(String[] args) throws Exception {
- //1.獲取Class對象
- Class stuClass = Class.forName("com.dr.Reflection.getMethodByReflect.Student");
- //2.獲取所有公有方法
- System.out.println("***************獲取所有的”公有“方法*******************");
- stuClass.getMethods();
- Method[] methodArray = stuClass.getMethods();
- for(Method m : methodArray){
- /*
- 注意,這里雖然student類自身只寫了一個public方法,但是由于java所有的類都繼承于object類,
- 因此object類中所有的public方法也會被打印出來
- */
- System.out.println(m);
- }
- System.out.println("***************獲取所有的方法,包括私有的*******************");
- methodArray = stuClass.getDeclaredMethods();
- for(Method m : methodArray){//不包含父類的,僅僅是這個自身定義的方法
- System.out.println(m);
- }
- System.out.println("***************獲取公有的show1()方法*******************");
- Method m = stuClass.getMethod("show1", String.class);//根據方法名稱,以及參數,獲取方法對象
- System.out.println(m);
- //實例化一個Student對象
- Object obj = stuClass.getConstructor().newInstance();
- m.invoke(obj, "劉德華");
- System.out.println("***************獲取私有的show4()方法******************");
- m = stuClass.getDeclaredMethod("show4", int.class);
- System.out.println(m);
- m.setAccessible(true);//解除私有限定
- Object result = m.invoke(obj, 20);//需要兩個參數,一個是要調用的對象(獲取有反射),一個是實參
- System.out.println("返回值:" + result);
- }
- }
一些問題
private修飾的方法可以通過反射訪問,那么private意義何在?
答:首先java的private修飾符并不是為了安全性設計的,private并不是解決“安全”問題的。private想表達的不是“安全性”的意思,而是面向對象編程的封裝概念,是一種編譯器可以幫助我們在設計上的一個點。
private的設計理念是對一個類的封裝,而封裝帶來的好處是,在項目開發過程當中,修改一個類的private屬性是不影響使用的,因為不存在對private代碼的顯式引用。
反射技術主要是為實現一些開發工具以及框架服務。在實際的開發過程當中,我們應該盡量避免使用反射。而在使用反射時也要非常小心。另外,關注Java知音公眾號,回復“后端面試”,送你一份面試題寶典!
反射和多態的區別
- 同為運行時獲取信息,多態獲取的信息僅僅在于確定方法應用所指向的實際對象。而反射在于獲取一個類的所用信息。
- 多態是一種面向對象語言的機制。而反射技術是java提供的專門用于動態獲取類的信息的技術。