Java反射機制是Java語言被視為準動態語言的關鍵性質。Java反射機制的核心就是允許在運行時通過Java Reflection APIs來取得已知名字的class類的相關信息,動態地生成此類,並調用其方法或修改其域(甚至是本身聲明為private的域或方法)。 也許你使用Java已經很長時間了,
Java反射機制是Java語言被視為準動態語言的關鍵性質。Java反射機制的核心就是允許在運行時通過Java Reflection APIs來取得已知名字的class類的相關信息,動態地生成此類,並調用其方法或修改其域(甚至是本身聲明為private的域或方法)。
也許你使用Java已經很長時間了,可是幾乎不會用到Java反射機制。你會嗤之以鼻地告訴我,Java反射機制沒啥用。或許在J2EE、J2SE等平臺,Java反射機制沒啥用(具體我也不瞭解,不多做評論),但是在Android應用開發中,該機制會帶給你許多驚喜。
如果熟悉Android,那麼你應該知道,Google不知出於什麼原因,在系統源碼中一些類或方法中經常加上“@hide”註釋標記。它的作用是使這個方法或類在生成SDK時不可見,因此由此註釋的東西,你在編譯期是不可見的。這就出現了一些問題。一些明明可以訪問的東西編譯期卻無法訪問了!這使得你的程式有些本來可以完成的功能無法編譯通過。
當然,有一種辦法是自己去掉Android源碼中的所有“@hide”標記,然後重新編譯一份自己的SDK。另一種辦法就是使用Java反射機制。當然,你還可以利用反射來訪問存在訪問限制的方法和修改其域。不過這種使用方法比較特殊,我們在文章的最後單獨討論。
從Class類說起
如果你使用Java,那麼你應該知道Java中有一個Class類。Class類本身表示Java對象的類型,我們可以通過一個Object(子)對象的getClass方法取得一個對象的類型,此函數返回的就是一個Class類。當然,獲得Class對象的方法有許多,但是沒有一種方法是通過Class的構造函數來生成Class對象的。
也許你從來沒有使用過Class類,也許你曾以為這是一個沒什麼用處的東西。不管你以前怎麼認為,Class類是整個Java反射機制的源頭。一切關於Java反射的故事,都從Class類開始。
因此,要想使用Java反射,我們首先得到Class類的對象。下表列出了幾種得到Class類的方法,以供大家參考。
Class object 誕生管道 |
示例 |
運用getClass() 註:每個class 都有此函數 |
String str = "abc"; Class c1 = str.getClass(); |
運用 Class.getSuperclass() |
Button b = new Button(); Class c1 = b.getClass(); Class c2 = c1.getSuperclass(); |
運用static method Class.forName() (最常被使用) |
Class c1 = Class.forName ("java.lang.String"); Class c2 = Class.forName ("java.awt.Button"); Class c3 = Class.forName ("java.util.LinkedList$Entry"); Class c4 = Class.forName ("I"); Class c5 = Class.forName ("[I"); |
運用 .class 語法 |
Class c1 = String.class; Class c2 = java.awt.Button.class; Class c3 = Main.InnerClass.class; Class c4 = int.class; Class c5 = int[].class; |
運用 primitive wrapper classes 的TYPE 語法 |
Class c1 = Boolean.TYPE; Class c2 = Byte.TYPE; Class c3 = Character.TYPE; Class c4 = Short.TYPE; Class c5 = Integer.TYPE; Class c6 = Long.TYPE; Class c7 = Float.TYPE; Class c8 = Double.TYPE; Class c9 = Void.TYPE; |
獲取一些基本信息
在我們得到一個類的Class類對象之後,Java反射機制就可以大施拳腳了。首先讓我們來瞭解下如何獲取關於某一個類的一些基本信息。
Java class 內部模塊 |
Java class 內部模塊說明 |
相應之Reflection API,多半為Class methods。 |
返回值類型(return type) |
package |
class隸屬哪個package |
getPackage() |
Package |
import |
class導入哪些classes |
無直接對應之API。可間接獲取。 |
|
modifier |
class(或methods, fields)的屬性 |
int getModifiers() Modifier.toString(int) Modifier.isInterface(int) |
int String bool |
class name or interface name |
class/interface |
名稱getName() |
String |
type parameters |
參數化類型的名稱 |
getTypeParameters() |
TypeVariable <Class>[] |
base class |
base class(只可能一個) |
getSuperClass() |
Class |
implemented interfaces |
實現有哪些interfaces |
getInterfaces() |
Class[] |
inner classes |
內部classes |
getDeclaredClasses() |
Class[] |
outer class |
如果我們觀察的class 本身是inner classes,那麼相對它就會有個outer class。 |
getDeclaringClass() |
Class |
上表中,列出了一些Java class內部信息的獲取方式。所採用的方法幾乎都是調用Class對象的成員方法(由此你就可以瞭解到Class類的用處了吧)。當然,表中所列出的信息並不是全部,有很大一部分沒有列出,你可以通過查閱Java文檔得到更全面的瞭解。另外,下麵將重點介紹一下類的構造函數、域和成員方法的獲取方式。
類中最重要的三個信息
如果要對一個類的信息重要性進行排名的話,那麼這三個信息理應獲得前三的名次。它們分別是:構造函數、成員函數、成員變數。
也許你不同意我的排名,沒關係。對於Java反射來說,這三個信息與之前介紹的基本信息相比較而言,有著本質的區別。那就是,之前的信息僅僅是只讀的,而這三個信息可以在運行時被調用(構造函數和成員函數)或者被修改(成員變數)。所以,我想無可否認,至少站在Java反射機制的立場來說,這三者是最重要的信息。
下麵,讓我們分別瞭解一下這三個重要信息的獲取方式。另外,我們將在後面的章節,詳細介紹他們的調用方式或者修改方式。
構造函數
如果我們將Java對象視為一個二進位的生活在記憶體中生命體的話,那麼構造函數無疑可以類比為Java對象生命體的誕生過程。我們在構造函數調用時為對象分配記憶體空間,初始化一些屬性,於是一個新的生命誕生了。
Java是純面向對象的語言,Java中幾乎所有的一切都是類的對象,因此可想而知構造函數的重要性。
Java反射機制能夠得到構造函數信息實在應該是一件令人驚喜的事情。正因為此,反射機制實質上才擁有了孵化生命的能力。換句話言之,我們可以通過反射機制,動態地創建新的對象。
獲取構造函數的方法有以下幾個:
Constructor getConstructor(Class[] params)
Constructor[] getConstructors()
Constructor getDeclaredConstructor(Class[] params)
Constructor[] getDeclaredConstructors()
我們有兩種方式對這四個函數分組。
首先可以由構造函數的確定性進行分類。我們知道,一個類實際上可以擁有很多個構造函數。那麼我們獲取的構造函數是哪個呢?我們可以根據構造函數的參數標簽對構造函數進行明確的區分,因此,如果我們在Java反射時指定構造函數的參數,那麼我們就能確定地返回我們需要的那個“唯一”的構造函數。getConstructor(Class[] params) 和getDeclaredConstructor(Class[] params)正是這種確定唯一性的方式。但是,如果我們不清楚每個構造函數的參數表,或者我們出於某種目的需要獲取所有的構造函數的信息,那麼我們就不需要明確指定參數表,而這時返回的就應該是構造函數數組,因為構造函數很可能不止一個。getConstructors()和getDeclaredConstructors()就是這種方式。
另外,我們還可以通過構造函數的訪問許可權進行分類。在設計類的時候,我們往往有一些構造函數需要聲明為“private”、“protect”或者“default”,目的是為了不讓外部的類調用此構造函數生成對象。於是,基於訪問許可權的不同,我們可以將構造函數分為public和非public兩種。
getConstructor(Class[] params) 和getConstructors()僅僅可以獲取到public的構造函數,而getDeclaredConstructor(Class[] params) 和getDeclaredConstructors()則能獲取所有(包括public和非public)的構造函數。
成員函數
如果構造函數類比為對象的誕生過程的話,成員函數無疑可以類比為對象的生命行為過程。成員函數的調用執行才是絕大多數對象存在的證據和意義。Java反射機制允許獲取成員函數(或者說成員方法)的信息,也就是說,反射機制能夠幫助對象踐行生命意義。通俗地說,Java反射能使對象完成其相應的功能。
和獲取構造函數的方法類似,獲取成員函數的方法有以下一些:
Method getMethod(String name, Class[] params)
Method[] getMethods()
Method getDeclaredMethod(String name, Class[] params)
Method[] getDeclaredMethods()
其中需要註意,String name參數,需要寫入方法名。關於訪問許可權和確定性的問題,和構造函數基本一致。
成員變數
成員變數,我們經常叫做一個對象的域。從記憶體的角度來說,構造函數和成員函數都僅僅是Java對象的行為或過程,而成員變數則是真正構成對象本身的細胞和血肉。簡單的說,就是成員變數占用的空間之和幾乎就是對象占用的所有記憶體空間。
獲取成員變數的方法與上面兩種方法類似,具體如下:
Field getField(String name)
Field[] getFields()
Field getDeclaredField(String name)
Field[] getDeclaredFields()
其中,String name參數,需要寫入變數名。關於訪問許可權和確定性的問題,與前面兩例基本一致。
讓動態真正動起來
在本文的一開始就說,Java反射機制是Java語言被視為準動態語言的關鍵性質。如果Java反射僅僅能夠得到Java類(或對象)運行時的信息,而不能改變其行為和屬性,那麼它當然算不上“動態”。百度了一把何謂“動態語言”,解釋如下:動態語言,是指程式在運行時可以改變其結構:新的函數可以被引進,已有的函數可以被刪除等在結構上的變化。由此看來,Java確實不能算作“動態語言”。但是和C、C++等純靜態語言相比,Java語言允許使用者在運行時載入、探知、使用編譯期間完全未知的classes,所以我們說Java是“準動態”語言。
細心地讀者可能已經發現,在“類中最重要的三個信息”一節中,我們獲取的信息其實都是屬於類的,而不是對象。對於類的信息提取,其實並不涉及到對象記憶體,在程式編譯完成的那一刻起,一切都已經是確定的了。因此,它並不能算“動態”。而如何對對象記憶體進行操作和訪問,才是“動”的真正含義。
說了這麼多,關鍵還在於如何利用反射讓Java真正動起來。下麵我將按照創生、行為與屬性三個方面來介紹反射機制是如何讓Java動的。
創生
不知是否本性使然,人類偏愛於思索起源與終結的話題。如果將程式類比於一個二進位的世界的話,那麼我們程式員則是這個世界的上帝。我們掌控著這個世界的起源和終結,熟悉世界中一草一木的屬性和所有生靈的習性。現在就讓我們開始創世紀吧!
在 “構造函數”那一小節中,我們列出了獲取構造函數的四種方法。這四種方法的返回值不知是否引起了各位的註意,那就是Constructor類。Constructor就類比於女媧吹給泥人的那一口真氣,有了它,一個生命才真正出現。
Constructor支持泛型,也就是它本身應該是Constructor<T>。這個類有一個public成員函數,T newInstance(Object... args),其中args為對應的參數。我們正是通過它來實現創生的過程。
行為
行為踐行著生命的意義,而眾多事物的行為才得以構成整個世界的運轉。儘管道家的老子主張“無為而治”,宣揚“聖人處無為之事,行不言之教”,但那是因為他本身就是 “無”的信仰者(“道”即“無”)。我們是唯物主義的信徒,所以必然要以“有”為價值。那麼,在二進位的世界里,我們如何調用Java對象的行為呢?
同樣,我們首先回顧“成員函數”小節中四種方法的返回值。對,那就是Method類。此類有一個public成員函數,Object invoke(Object receiver, Object... args)。我們能很好理解此函數的第二個參數args,它代表這個方法所需要接收的參數。也許大家對第一個參數receiver還存在疑惑之處。這得從編程語言的發展歷程講起。
如果你關註幾種主流編程語言的起源,那麼你能有這樣的印象:C從彙編而來,C++從C而來,而Java從C/C++而來。有這樣一種印象就足夠了。從這樣的發展史我們可以看出,C++和Java這兩種面向對象的編程語言都是從面向過程的C語言基礎上發展而來的。OOP是一種思想,它本身與編程語言無關。也就是說,我們用C也能寫出面向對象的程式,這也是C++和Java能夠以C為基礎的根本所在。然而,C無法實現類似object.method()這種表現形式,因為C語言的結構體中並不支持函數定義。那麼我們用C實現OOP的時候,如何調用對象的方法呢?
本質上說,object.method()這種調用方式是為了表明具體method()的調用對象。而invoke(Object receiver, Object... args)的第一個參數正是指明調用對象。在C++中,object.method()其實是有隱含參數的,那就是object對象的指針,method原型的第一個參數其實是this指針,於是原型為method(void* this)。
這樣一溯源,也許你更清楚了Object receiver參數的含義,或許更迷糊了?不管怎樣,歷史就是如此,只不過我個人能力有限,說不清楚而已。
另外需要註意的是,如果某個方法是Java類的靜態方法,那麼Object receiver參數可以傳入null,因為靜態方法不從屬於對象。
屬性
同樣是人類,令狐沖和岳不群是如何被區分開的?那是因為他們有著不同的屬性。同樣,同一個類可以生成多個對象,幾個同類型的對象之間如何區分?屬性起著決定性的作用。說到這裡,想起一個科幻故事。人體瞬移機,作用的根本原理就是人進入A位置,被完全掃描之後,再在B位置重新組成它的細胞、血肉等屬性,從而完全創造出另一個一模一樣的人。當然,這是唯物主義的極致,它假設了只要一切物質相同,連記憶和靈魂都不會出現偏差,另外還存在倫理的問題,例如A位置的人會被銷毀掉嗎?
儘管這是一個科幻,但是在程式的世界里,我們早已經用上了這類似幻想的技術。Java中如何遠程傳遞一個對象?我們已經使用上了Java對象序列化的介面。不僅如此,利用序列化介面,我們甚至可以將一個生命保存起來,在需要的時候將它複活,這就是對象的持久化。不得不感慨,在程式的世界里,我們就是上帝啊!
對象序列化如此強大,那麼它的本質是什麼呢?它的工作原理是怎樣的呢?簡單的說,對象序列化的本質就是屬性的序列化。原理就是我們崇尚的唯物主義,如果同一個類的兩個對象所有屬性值都完全相同,那麼我們可以認為這是同一個對象。
說了這麼多,只是想說明一件事情,屬性對於對象而言是多麼的重要。那麼如何讀寫對象中屬性的值呢?回顧獲取屬性信息的方法返回值類型,那是Field。Field類有兩個public方法,分別對應讀與寫,它們是:
Object get(Object object)
void set(Object object, Object value)
object參數需要傳入的對象,原理類似於成員方法需要指明對象一樣。如果是靜態屬性,此值同樣可以為null。
關於反射的一些高級話題
如果說前面那些屬於Java反射的基本知識,那麼在文章的最後,我們來探討一下反射的一些高級話題。另外,本文對基礎知識的講解僅屬於抓主幹,具體的一些旁支可以自己參看文檔。需要提一下的是,Java反射中對數組做過單獨的優化處理,具體可查看java.lang.reflect.Array類;還有關於泛型的支持,可查看java.lang.reflect.ParameterizedType及相關資料。
暫時想到的高級話題有三個,由於對Java反射理解的也不算深入,所以僅僅從思路上進行探討,具體實現上,大家可以參考其他相關資料,做更深入研究。
Android編譯期問題
Android的安全許可權問題我把它簡單的劃分成三個層次,最不嚴格的一層就是僅僅騙過編譯器的“@hide”標記。對於一款開源的操作系統而言,這個標記本身並不具備安全上的限制。不過,從上次Google過來的負責Android工程師的說法來看,這個標記的作用更多的是方便硬體廠商做閉源的二次開發。這樣解釋倒也說得過去。
不過這並不影響我們使用反射機制以繞過原生Android的第一層安全措施。如果你熟悉源碼的話,會發現這可以應用到很多地方。並且最關鍵的是你並不需要放在源碼中編譯,而是像普通應用程式的開發過程一樣。
具體使用範圍我不能一一列舉了,例如自定義視窗、安裝程式等等。簡單的說,在Android上使用反射技術,你才會對Android系統有更深的理解和更高的控制權。
軟體的解耦合
我們在架構代碼的時候,經常提到解耦合、弱耦合。其實,解耦和不僅僅只能在代碼上做文章。我們可以考慮這樣一種情況:軟體的功能需求不可能一開始就完全確定,有一些功能在軟體開發的後期甚至是軟體已經發佈出去之後才想到要加入或者去掉。
按我們慣有的思維,這種情況就得改動源碼,重新編譯。如果軟體已經發佈出去,那麼就得讓客戶重新安裝一次軟體。反思一下,我們是否認為軟體和程式是同一回事呢?事實上,如果你能將軟體和程式分開來理解,那麼你會發現,為了應對以上的情況,我們還有其他的解決辦法。
我國有一個很重要但是很麻煩的制度,那就是戶籍制度。它的本意是為了更好的管理人口事宜。每當一個孩子出生,我們就需要在戶籍管理的地方去給他辦理戶籍入戶;而每當一個人去世,我們也需要在相應的地方銷去他的戶籍。既然我們可以視類為生命,那麼我們能否通過學習這樣的戶籍管理制度來動態地管理類呢?
事實上這樣的管理是可行的,而且Java虛擬機本身正是基於這樣的機制來運行程式的。因此我們是否可以這樣來架構軟體框架。首先,我們的軟體有一個配置文件,配置文件其實是一個文本,裡面詳細描述了,我們的軟體核心部分運行起來後還需要從什麼路徑載入些什麼類需要何時調用什麼方法等。這樣當我們需要加或減某些功能時,我們只需要簡單地修改配置文本文件,然後刪除或者添加相應的.class文件就可以了。
如果你足夠敏感,你或許會發現,這種方式形成的配置文件幾乎可以相當於一門腳本語言了。而且這個腳本的解釋器也是我們自己寫的,另外關鍵是它是開發的,你可以為它動態地加入一些新的類以增加它的功能。
不要以為這僅僅是一個設想,雖然要開發成一門完備的腳本語言確實比較麻煩。但是在一些網路端的大型項目中,通過配置文件 + ClassLoader + 反射機制結合形成的這種軟體解耦和方式已經用得比較普遍了。
所以,在此我不是在提出一種設想,而是在介紹業界處理此類問題的一種解決方案。
反射安全
文章讀到這裡,我想你應該由衷地感嘆,Java反射機制實在是太強大了。但是,如果你有一些安全意識的話,就會發現Java這個機制強大得似乎有些過頭了。前面我們提到,Java反射甚至可以訪問private方法和屬性。為了讓大家對Java反射有更全面的瞭解,樹立正確的人生觀價值觀,本小節將對Java的安全問題做一個概要性的介紹。
相對於C++來說,Java算是比較安全的語言了。這與它們的運行機制有密切的關係,C++運行於本地,也就是說幾乎所有程式的許可權理論上都是相同的。而Java由於是運行於虛擬機中,而不直接與外部聯繫,所以實際上Java的運行環境是一個“沙盒”環境。
Java的安全機制其實是比較複雜的,至少對於我來說是如此。作為Java的安全模型,它包括了:位元組碼驗證器、類載入器、安全管理器、訪問控制器等一系列的組件。之前文中提到過,我把Android安全許可權劃分為三個等級:第一級是針對編譯期的“@hide”標記;第二級是針對訪問許可權的private等修飾;第三級則是以安全管理器為托管的Permission機制。
Java反射確實可以訪問private的方法和屬性,這是繞過第二級安全機制的方法(之一)。它其實是Java本身為了某種目的而留下的類似於“後門”的東西,或者說是為了方便調試?不管如何,它的原理其實是關閉訪問安全檢查。
如果你具有獨立鑽研的精神的話,你會發現之前我們提到的Field、Method和Constructor類,它們都有一個共同的父類AccessibleObject 。AccessibleObject 有一個公共方法:void setAccessible(boolean flag)。正是這個方法,讓我們可以改變動態的打開或者關閉訪問安全檢查,從而訪問到原本是private的方法或域。另外,訪問安全檢查是一件比較耗時的操作,關閉它反射的性能也會有較大提升。
不要認為我們繞過了前兩級安全機制就沾沾自喜了,因為這兩級安全並不是真正為了安全而設置的。它們的作用更多的是為了更好的完善規則。而第三級安全才是真正為了防止惡意攻擊而出現的。在這一級的防護下,你甚至可能都無法完成反射(ReflectPermission),其他的一切自然無從說起。
反射應用示例(補充)
package com.test.demo; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * 反射機制的應用示例 * @author Administrator * */ public class Test { String name; int age; public Test() { // TODO 自動生成的構造函數存根 } public Test(String name,int age) { this.name=name; this.age=age; } public String getName() { return name; } public int getAge() { return age; } public void fun() { System.out.println("hello reflect"); } public static void main(String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, ClassNotFoundException, InstantiationException { /** * 根據方法名稱來執行方法 */ Class<Test> a=Test.class; Method method=a.getMethod("fun", null); Test test=new Test(); method.invoke(test, null); /** * 創建新的對象 */ Class b= Class.forName("com.test.demo.Test"); Class partypes[]=new Class[2]; partypes[0]=String.class; partypes[1]=Integer.TYPE; Constructor constructor=b.getConstructor(partypes); Object argList[]=new Object[2]; argList[0]=new String("xujian"); argList[1]=new Integer(21); Test object=(Test) constructor.newInstance(argList); System.out.println(object.getName()+":"+object.getAge()); } }