Java反射與註解

shaoneng111發表於2018-08-08

反射

能夠分析類能力的程式稱為反射(reflective),程式碼的這種能力稱為"自省"。反射機制的功能極其強大,反射機制可以用來:

  • 在執行時分析類的能力
  • 在執行時檢視物件,例如,編寫一個toString方法供所有類使用
  • 實現通用的陣列操作程式碼
  • 利用Method物件,這個物件很像C++中的函式指標

Class類

在程式執行期間,Java執行時系統為所有物件維護一個被稱為執行時的型別標識。虛擬機器利用執行時型別資訊選擇相應的方法執行。

然而,可以通過專門的Java類訪問這些資訊。儲存這些資訊的類被稱為Class,這個名字很容易讓人混淆。Object類中的getClass()方法將會返回一個Class型別的例項。最常用的Class方法是getName,獲取類的名字。

Employee e;
...
Class cl = e.getClass();
String name = cl.getName();
複製程式碼

如果類在一個包裡,包的名字也作為類名的一部分:

Random generator = new Random();
Class cl = generator.getClass();
String name = cl.getName();  // name is set to "java.util.Random"
複製程式碼

還可以呼叫靜態方法forName獲得類名對應的Class物件。

String className = "java.util.Random";
Class cl = Class.forName(className);
複製程式碼

獲得Class類物件的第三種方法,如果T是任意的Java型別(或void關鍵字),T.class將代表匹配的類物件。

Class cl1 = Random.class;  //if you import java.util.*
Class cl2 = int.class; 
Class cl3 = Double[].class;
複製程式碼

注意:Class物件實際上表示的是一個型別,而這個型別未必一定是一種類。例如,int不是類,但int.class是一個Class型別的物件。

虛擬機器為每個型別管理一個Class物件。因此,可以利用==運算子實現兩個類物件比較的操作。例如:

if(e.getClass() == Employee.class) ...
複製程式碼

還有一個很有用的方法newInstance(),可以用來動態建立一個類的例項,例如,

e.getClass().newInstance();
複製程式碼

建立了一個與e具有相同類型別的例項。newInstance方法呼叫預設的構造器(沒有引數的構造器)初始化新建立的物件。如果這個類沒有預設的構造器,就會丟擲一個異常。(注意:要求我們除非強制要求建立物件是必須實現帶引數的,否則一般會在實現帶參建構函式的同時,實現無參預設建構函式。)

將forName與newInstance配合起來使用,可以根據儲存在字串中的類名建立一個物件。

String s = "java.util.Random";
Object m = Class.forName(s).newInstance();
複製程式碼

注意:在JDK9以後newInstance() 廢棄,@Deprecated(since="9"),替換為,

clazz.getDeclaredConstructor().newInstance()
String employeeClassStr = "EmploySalary.Employee";
//如果建構函式有引數
Object m = Class.forName(employeeClassStr).getDeclaredConstructor(String.class, double.class).newInstance("abc", 1.2);
複製程式碼

反射的用處之一在於:可以不在編譯期用import匯入需要的類或在maven中寫下需要的jar包,而在執行期通過類名建立需要的類,如下:

package RefectTest;
//沒有import EmploySalary包
public class ReflectionT1 {
	public static void main(String[] args) {
		String className = "EmploySalary.EmployeeTest";  //不在RefectTest包中
		try {
			Object o = Class.forName(className).getConstructor().newInstance();
			System.out.println(o.getClass().getName());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
複製程式碼

利用反射分析類的能力

反射機制最重要的內容——檢查類的結構。

在java.lang.reflect包中有三個類Field、Method、Constructor分別用於描述類的域、方法和構造器。

Class類中的getFields、getMethods和getConstructors方法將分別返回類提供的public域、方法和構造器陣列,其中包括超類的公有成員。Class類的getDeclareFields、getDeclareMethods和getDeclaredConstuctors方法分別返回類中宣告的全部域、方法和構造器,其中包括私有和受保護成員,但不包括超類的成員。另外,還可以利用Modifier.toString方法將修飾符列印出來。

在執行時使用反射分析物件

如果知道想要檢視的域名和型別,檢視指定的域是一件很容易的事情。而利用反射機制可以檢視編譯時還不清楚的物件域。

Employee harry = new Employee("Harry Hack", 562);
Class cl = harry.getClass();
Field f = cl.getDeclaredField("name");
f.setAccessible(true);    //由於name是私有的,如果沒有這條語句,將拋IllegalAccessException
Object v = f.get(harry);
System.out.println(v.getClass().getName());   //輸出java.lang.String
System.out.println(v.toString());   //輸出Harry Hack
f.set(harry, "Jackie");   //將obj物件的f域設定為新值
v = f.get(harry);
System.out.println(v.toString());  //輸出Jackie
複製程式碼

反射機制的預設行為受限於Java的訪問控制。然而,如果一個Java程式沒有受到安全管理器的控制,就可以覆蓋訪問控制。為了達到這個目的,需要呼叫Field、Method或Constructor物件的setAccessible方法。

setAccessible方法是AccessibleObject類中的一個方法,它是Field、Method和Construct類的公共超類。這個特性是為除錯、持久儲存和相似機制提供的。

使用反射編寫泛型陣列程式碼

java.lang.reflect包中的Array類允許動態地建立陣列。

Arrays的copyOf方法,可以用於擴充套件已經填滿的陣列

Employee[] a = new Employee[100];
...
//array is full
a = Arrays.copyOf(a, 2 * a.length);
複製程式碼

如何編寫一個通用的方法呢?正好能夠將Employee[]陣列轉換成Object[]陣列。

public static Object[] badCopyOf(Object[] a, int newLength){		
	Object[] newArray = new Object[newLength];
	System.arraycopy(a, 0, newArray, 0, Math.min(a.length, newLength));
	return newArray;		
}
複製程式碼

然而,在實際使用結果陣列時會遇到一個問題。這段程式碼返回的陣列型別是物件陣列(Object[])型別,這是由於使用下面這行程式碼建立的陣列:

new Object[newLength]
複製程式碼

一個物件陣列不能轉換成僱員陣列(Employee[])。如果這樣做,執行時Java將會產生ClassCastException異常。前面已經看到,Java陣列會記住每個元素的型別,即建立陣列時new表示式中使用的元素型別。將一個Employee[]臨時地轉換成Object[]陣列,然後再將它轉換回來是可以的,但一個從開始就是Object[]的陣列卻永遠無法轉換成Employee[]陣列。

使用java.lang.reflect包中的Array類,可以實現這個我們的目的。

public static Object goodCopyOf(Object a, int newLength) {
	Class cl = a.getClass();
	if(!cl.isArray()) 
		return null;
	Class componentType = cl.getComponentType();
	int length = Array.getLength(a);
	Object newArray = Array.newInstance(componentType, newLength);
	System.arraycopy(a, 0, newArray, 0, Math.min(length, newLength));
	return newArray;		
}
複製程式碼

這個goodCopyOf方法可以用來擴充套件任意型別的陣列,而不僅是物件陣列。

int[] a = {1,3,4};
a = (int[]) goodCopyOf(a, 10);
複製程式碼

為了實現上述操作,應該將goodCopyOf引數宣告為Object型別,而不要宣告為物件型陣列(Object[])。整型陣列型別int[]可以被轉換成Object,但不能轉換成物件陣列

呼叫任意方法

在C和C++中,可以從函式指標執行任意函式。從表面上看,Java沒有提供方法指標,即將一個方法的儲存地址傳給另外一個方法,以便第二個方法能夠隨後呼叫它。事實上,Java設計者曾說過:方法指標是很危險的,並且常常帶來隱患。他們認為Java提供的介面(interface)是一種更好的解決方案。然而,反射機制允許你呼叫任意方法。 為了能夠看到方法指標的工作過程,先回憶一下利用Field類的get方法檢視物件域的過程。與之類似,在Method類中有一個invoke方法,它允許呼叫包裝在當前Method物件中的方法。invoke方法的簽名是:

Object invoke(Object, Object ... args)
複製程式碼

對於靜態方法,第一個引數可以被忽略,即可以將它設定為null。

假設m1代表Employee類的getName方法,m2代表Employee的getSalary方法。

String n = (String)m1.invoke(harry);
double s = (Double)m1.invoke(harry);
複製程式碼

如何獲得Method物件?

Method getMethod(String name, Class ... parameterTypes)
複製程式碼

下面說明了如何獲得Emplopyee類的getName方法和raiseSalary方法的方法指標。

Method m1 = Employee.class.getMethod("getName");
Method m2 = Employee.class.getMethod("raiseSalary", double.class);
複製程式碼

建議僅在必要的時候才使用Method物件,而最好使用介面以及Java SE 8中的lambda表示式。建議Java開發者不要使用Method物件的回撥功能。使用介面進行回撥會使得程式碼執行速度更快,更易維護。

註解

註解是那些插入到原始碼中使用其他工具可以對其進行處理的標籤。這些工具可以在原始碼層次上進行操作,或者可以處理編譯器在其中放置了註解的類檔案。

註解不會改變程式的編譯方式。Java編譯器對於包含註解和不包含註解的程式碼會生成相同的虛擬機器指令。

為了能夠受益於註解,你需要選擇一個處理工具,然後向你的處理工具可以理解的程式碼中插入註解,之後運用該處理工具處理程式碼。

註解的使用範圍:

  • 附屬檔案的自動生成,例如部署描述符或bean資訊類
  • 測試、日誌、事務語義等程式碼的自動生成

每個註解都必須通過一個註解介面進行定義。這些介面中的方法與註解中的元素相對應。例如,JUnit的註解Test可以用下面這個介面定義:

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Test
    {
        long timeout() default OL;
    }
複製程式碼

@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註解會告知編譯器阻止特殊型別的警告資訊,例如,

@SuppressWarning("unchecked")
複製程式碼

用於管理資源的註解

@PostConstruct和@PreDestroy註解用於控制物件生命週期的環境中,例如Web容器和應用伺服器。標記了這些方法應該在物件被構建之後,或者在物件被移除之前,緊接著呼叫。

@Resource註解用於資源注入。在Web應用中,可以像下面這樣引用資料來源:

@Resource(name="jdbc/mydb")
private DataSource source;  //當這個過程域物件被建立時,容器會“注入”一個對該資料來源的引用。
複製程式碼

@Generated註解的目的是供程式碼生成工具來使用。任何生成的原始碼都可以被註解,從而與程式設計師提供的程式碼區分開。

元註解

@Target 元註解可以應用與一個註解,以限制該註解可以應用到哪些項上

@Rentention元註解用於指定一條註解應該保留多長時間。

@Documented 註解為像Javadoc這樣的歸檔工具提供了一些提示。

@Inherited元註解只能應用於對類的註解。如果一個類具有繼承註解,那麼它的所有子類都自動具有同樣的註解。

代理

利用代理可以在執行時建立一個實現了一組給定介面的新類。這種功能只有在編譯時無法確定需要實現哪個介面時才有必要使用。

相關文章