能夠分析類能力的程式稱為反射(reflective),代碼的這種能力稱為"自省"。反射機制的功能極其強大,反射機制可以用來:
在運作時分析類的能力
在運作時檢視對象,例如,編寫一個toString方法供所有類使用
實作通用的數組操作代碼
利用Method對象,這個對象很像C++中的函數指針
在程式運作期間,Java運作時系統為所有對象維護一個被稱為運作時的類型辨別。虛拟機利用運作時類型資訊選擇相應的方法執行。
然而,可以通過專門的Java類通路這些資訊。儲存這些資訊的類被稱為Class,這個名字很容易讓人混淆。Object類中的getClass()方法将會傳回一個Class類型的執行個體。最常用的Class方法是getName,擷取類的名字。
如果類在一個包裡,包的名字也作為類名的一部分:
還可以調用靜态方法forName獲得類名對應的Class對象。
獲得Class類對象的第三種方法,如果T是任意的Java類型(或void關鍵字),T.class将代表比對的類對象。
注意:Class對象實際上表示的是一個類型,而這個類型未必一定是一種類。例如,int不是類,但int.class是一個Class類型的對象。
虛拟機為每個類型管理一個Class對象。是以,可以利用==運算符實作兩個類對象比較的操作。例如:
還有一個很有用的方法newInstance(),可以用來動态建立一個類的執行個體,例如,
建立了一個與e具有相同類類型的執行個體。newInstance方法調用預設的構造器(沒有參數的構造器)初始化新建立的對象。如果這個類沒有預設的構造器,就會抛出一個異常。(注意:要求我們除非強制要求建立對象是必須實作帶參數的,否則一般會在實作帶參構造函數的同時,實作無參預設構造函數。)
将forName與newInstance配合起來使用,可以根據存儲在字元串中的類名建立一個對象。
注意:在JDK9以後newInstance() 廢棄,@Deprecated(since="9"),替換為,
反射的用處之一在于:可以不在編譯期用import導入需要的類或在maven中寫下需要的jar包,而在運作期通過類名建立需要的類,如下:
反射機制最重要的内容——檢查類的結構。
在java.lang.reflect包中有三個類Field、Method、Constructor分别用于描述類的域、方法和構造器。
Class類中的getFields、getMethods和getConstructors方法将分别傳回類提供的public域、方法和構造器數組,其中包括超類的公有成員。Class類的getDeclareFields、getDeclareMethods和getDeclaredConstuctors方法分别傳回類中聲明的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。另外,還可以利用Modifier.toString方法将修飾符列印出來。
如果知道想要檢視的域名和類型,檢視指定的域是一件很容易的事情。而利用反射機制可以檢視編譯時還不清楚的對象域。
反射機制的預設行為受限于Java的通路控制。然而,如果一個Java程式沒有受到安全管理器的控制,就可以覆寫通路控制。為了達到這個目的,需要調用Field、Method或Constructor對象的setAccessible方法。
setAccessible方法是AccessibleObject類中的一個方法,它是Field、Method和Construct類的公共超類。這個特性是為調試、持久存儲和相似機制提供的。
java.lang.reflect包中的Array類允許動态地建立數組。
Arrays的copyOf方法,可以用于擴充已經填滿的數組
如何編寫一個通用的方法呢?正好能夠将Employee[]數組轉換成Object[]數組。
然而,在實際使用結果數組時會遇到一個問題。這段代碼傳回的數組類型是對象數組(Object[])類型,這是由于使用下面這行代碼建立的數組:
一個對象數組不能轉換成雇員數組(Employee[])。如果這樣做,運作時Java将會産生ClassCastException異常。前面已經看到,Java數組會記住每個元素的類型,即建立數組時new表達式中使用的元素類型。将一個Employee[]臨時地轉換成Object[]數組,然後再将它轉換回來是可以的,但一個從開始就是Object[]的數組卻永遠無法轉換成Employee[]數組。
使用java.lang.reflect包中的Array類,可以實作這個我們的目的。
這個goodCopyOf方法可以用來擴充任意類型的數組,而不僅是對象數組。
為了實作上述操作,應該将goodCopyOf參數聲明為Object類型,而不要聲明為對象型數組(Object[])。整型數組類型int[]可以被轉換成Object,但不能轉換成對象數組。
在C和C++中,可以從函數指針執行任意函數。從表面上看,Java沒有提供方法指針,即将一個方法的存儲位址傳給另外一個方法,以便第二個方法能夠随後調用它。事實上,Java設計者曾說過:方法指針是很危險的,并且常常帶來隐患。他們認為Java提供的接口(interface)是一種更好的解決方案。然而,反射機制允許你調用任意方法。為了能夠看到方法指針的工作過程,先回憶一下利用Field類的get方法檢視對象域的過程。與之類似,在Method類中有一個invoke方法,它允許調用包裝在目前Method對象中的方法。invoke方法的簽名是:
對于靜态方法,第一個參數可以被忽略,即可以将它設定為null。
假設m1代表Employee類的getName方法,m2代表Employee的getSalary方法。
如何獲得Method對象?
下面說明了如何獲得Emplopyee類的getName方法和raiseSalary方法的方法指針。
建議僅在必要的時候才使用Method對象,而最好使用接口以及Java SE 8中的lambda表達式。建議Java開發者不要使用Method對象的回調功能。使用接口進行回調會使得代碼執行速度更快,更易維護。
注解是那些插入到源代碼中使用其他工具可以對其進行處理的标簽。這些工具可以在源碼層次上進行操作,或者可以處理編譯器在其中放置了注解的類檔案。
注解不會改變程式的編譯方式。Java編譯器對于包含注解和不包含注解的代碼會生成相同的虛拟機指令。
為了能夠受益于注解,你需要選擇一個處理工具,然後向你的處理工具可以了解的代碼中插入注解,之後運用該處理工具處理代碼。
注解的使用範圍:
附屬檔案的自動生成,例如部署描述符或bean資訊類
測試、日志、事務語義等代碼的自動生成
每個注解都必須通過一個注解接口進行定義。這些接口中的方法與注解中的元素相對應。例如,JUnit的注解Test可以用下面這個接口定義:
@interface聲明建立了一個真正的Java接口。處理注解的工具将接收那些實作了這個注解接口的對象。這類工具可以調用timeout方法來檢索某個特定Test注解的timeout元素。
注解Target和Retention是元注解。它們注解了Test注解,即将Test注解辨別成一個隻能運用到方法上的注解,并且當類檔案載入到虛拟機的時候,仍可以保留下來。
注解接口中的元素聲明實際上是方法聲明。一個注解接口的方法不能有任何參數和任何throws語句,并且它們也不能是泛型的。注意:一個注解元素永遠不能設定為null,甚至不允許其預設值為null。這樣在實際應用用會相當不友善。你必須使用其他的預設值,例如""或者Void.class
Java SE在java.lang.annotation和javax.annotation包中定義了大量的注解接口。其中四個是元注解,用于描述注解接口的行為屬性,其他的三個是規則接口,可以用它們來注解你的源代碼中的項。
@Deprecated注解可以被添加到任何不再鼓勵使用的項上。是以,當你使用一個已過時的項時,編譯器将會發出警告。這個注解與Java文檔标簽@deprecated具有同等功效。
@SuppressWarning注解會告知編譯器阻止特殊類型的警告資訊,例如,
@PostConstruct和@PreDestroy注解用于控制對象生命周期的環境中,例如Web容器和應用伺服器。标記了這些方法應該在對象被建構之後,或者在對象被移除之前,緊接着調用。
@Resource注解用于資源注入。在Web應用中,可以像下面這樣引用資料源:
@Generated注解的目的是供代碼生成工具來使用。任何生成的源代碼都可以被注解,進而與程式員提供的代碼區分開。
@Target 元注解可以應用與一個注解,以限制該注解可以應用到哪些項上
@Rentention元注解用于指定一條注解應該保留多長時間。
@Documented 注解為像Javadoc這樣的歸檔工具提供了一些提示。
@Inherited元注解隻能應用于對類的注解。如果一個類具有繼承注解,那麼它的所有子類都自動具有同樣的注解。
利用代理可以在運作時建立一個實作了一組給定接口的新類。這種功能隻有在編譯時無法确定需要實作哪個接口時才有必要使用。