21天學會Java之(Java SE第十四篇):註解、反射機制

註解和反射機制使用特別簡單,但是它們在框架中被大量的使用,而如何靈活運用,想要深入理解框架,牢牢的掌握註解和反射機制的知識就顯得極其的重要了。

註解

註解不同於註釋,註釋僅只用於寫在源代碼中,來使自己或者別人更容易的翻閱源代碼。

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

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

註解主要用途有以下兩點:

  • 附屬文件的自動生成,例如部署描述符或者bean信息類。
  • 測試、日誌、事務語義等代碼的自動生成。

使用註解的前提下,首先我們應該知道註解本身不會做任何事情,它們只是存在於源文件中。編譯器將它們置於類文件中,並且虛擬機會將它們載入。

註解的使用

註解接口

註解是由註解接口來定義的,語法格式如下:

修飾符 @interface 註解名{
    元素類型聲明1
    元素類型聲明2
    ...
}

每個元素類型聲明語法格式如下(元素類型見下文):

元素類型 對象名();   //不帶默認值就是類型的默認值
或者
元素類型 對象名() default value;   //帶默認值

舉個栗子,下面的註解具有三個元素,id、name、age:

public @interface Test{
    int id();
    String name() default "小王";
    int age() default 21;
}

所有的註解接口都隱性地擴展自java.lang.annotation.Annotation接口。這個接口是一個常規接口,不是一個註解接口。下表是這個接口的一些常用方法:

方法 用途
Class<? extends Annotation> annotationType() 返回Class對象,它用於描述該註解對象的註解接口。注意:調用註解對象上的getClass方法可以返回真正的類,而不是接口
boolean equals(Object other) 如果other是一個實現了與該註解對象相同的註解接口的對象,並且如果該對象和other的所有元素彼此相等。那麼返回True
int hashCode() 返回一個與equals方法兼容、由註解接口名以及元素值衍生而來的散列碼
String toString() 返回一個包含註解接口名以及元素值的字符串表示,例如,@Test(id=0,name=“小王”,age=21)

所有的註解接口都直接擴展自java.lang.annotation.Annotation,我們不需要爲註解接口提供實現類。

註解元素的類型爲下列之一:

  • 基礎數據類型(byte、short、int、long、char、double、float或boolean)
  • String
  • Class(具有一個可選的類型參數,例如Class<? extends MyClass> )
  • enum類型
  • 註解類型
  • 有前面所述類型組成的數組(由數組組成的多維數組不是合法的元素類型)

註解

註解的格式

每個註解都具有這種格式@註解名(元素名1=值1,元素名2=值2,....)

例如上文的註解他應該這種格式@Test(id=0,name="小王",age=21)

而元素的順序無關緊要,@Test(name="小王",age=21,id=0)這個註解和前面那個註解一樣。

如果某個元素的值並未指定,那麼就使用聲明的默認值,或者元素類型的默認值。例如@Test(id=0),那麼元素name的值就是字符串小王,元素age的值就是21。

注意事項:默認值並不是和註解存儲在一起的;它們是動態計算而來的。例如,如果你將元素age的默認值改爲21,然後重新編譯Test接口,那麼註釋@Test(id=0)將使用這個新的默認值,甚至在那些在默認值修改之前就已經編譯過的類文件中也是如此。

註解格式簡化

有兩個特殊的快捷方式可以用來簡化註解。

  1. 如果沒有指定元素,要麼是因爲註解中沒有任何元素,要麼是因爲所有元素都使用默認值,那麼你就不需要使用圓括號了。例如@Test和這個註解是一樣的@Test(id=0,name="小王",age=21),這樣的註解又稱爲標記註解。
  2. 另外一種快捷方式是單值註解。如果一個元素具有特殊的名字value,並且沒有指定其他元素,那麼你就可以忽略掉這個元素名以及等號。例如:
定義一個註解接口如下形式:
public @interface TestOneValue(){
    String value();
}
那麼,我們可以將這個註解書寫成如下形式:
@TestOneValue("test")
而不是
@TestOneValue(value="test")

註解的使用

一個項可以有多個註解,例如:

@Test(id=0,name="小王",age=21)
@TestOneValue("test")
public void test01(){}

如果註解聲明爲可重複的,那麼我們就可以重複使用同一個註解:

@Test(id=0,name="小王",age=21)
@Test(id=1,name="小二",age=2)
public void test02(){}

注意事項:一個註解元素永遠不能設置爲null,並且不允許其默認值爲null。這樣在實際應用中會相當不方便。你必須使用其他的默認值,泥例如“”或者Void.class。

如果元素值是一個數組,那麼要將它的值用括號括起來,例如:@Test(...,score={100,101,102})

如果該元素只有一個值,那麼可以忽略這些括號,例如:@Test(...,score=100),這個就和@Test(...,score={100})一樣。

既然個註解可以是另一個註解,那麼就可以創建出任意複雜的註解,但是一般我們不這麼用理解即可,例如:@Test(ref=@TestOneValue("test")

注意事項:在註解中引入循環依賴是一種錯誤。例如,因爲Test具有一個註解類型爲Reference的元素,所以Reference就不能再擁有一個類型爲Test的元素。

註解各類聲明

註解可以出現在許多地方,這些地方可以分爲兩類:聲明和類型用法聲明註解可以出現在下列聲明處:

  • 類(包括enum)
  • 接口(包括註解接口)
  • 方法
  • 構造器
  • 實例域(包含enum常量)
  • 局部變量
  • 參數變量
  • 類型參數

對於類和接口,需要將註解放置在class和interface關鍵詞的前面:

@Test
public class Student {...}

對於變量,需要將它們放置在類型的前面:

@SuppressWarnings("unchecked")
int age;

泛型或者方法中的類型參數可以想下面這樣被註解:

public class Cache<@Immutable V>{...}

包是在文件package-info.java中註解的,該文件只包含以註解先導的包語句:

/**
	Package-level Javadoc
*/
@GPL(version="3")
package cn.ac.whz.annotation;
import org.gnu.GPL;

注意事項:對局部變量的註解只能在源碼級別上進行處理。類文件並不描述局部變量。因此,所有的局部變量註解在編譯完一個類的時候就會被遺棄掉。同樣的,對包的註解不能在源碼級別之外存在。

註解類型用法

聲明註解提供了正在被聲明的項的相關信息。例如下面的聲明中:

public User getUser(@NonNull String userId)

就斷言userId參數不爲空。

@NonNull註解是Checker Framework的一部分。通過使用這個框架,可以在程序中包含斷言,例如某個參數不爲空,或者某個String包含一個正則表達式。然後,靜態分析工具將檢查在給定的源代碼段中這些斷言是否有效。

現在,假設我們有一個類型爲List<String>的參數,並且想要表示其中所有的字符串都不爲null。這就是類型用法註解大顯身手之處,可以將該註解放置到類型參數之前:List<@NonNull String>

類型用法註解可以出現在下面的位置:

  • 和泛型參數一起使用:List<@NonNull String>,Comparator.<@NonNull String> reverseOrder()
  • 數組中的任何位置:@NonNull String[] [] words(word[i] [j]不爲null),String @NonNull [] [] words(words不爲null),String[] @NonNull [] words(word[i]不爲null)
  • 與超類和實現接口一起使用:class Warning extends @Localized Message
  • 與構造器調用一起使用:new @Localized String(…)。
  • 與強制類型和instanceof檢查一起使用:(@Localized String) text,if (text instanceof @Localized String)。(這些註解只提供外部工具使用,它們對強制轉型和instanceof檢查不會產生任何影響)。
  • 與異常規約一起使用:public String read() throws @Localized IOException
  • 與通配符和類型邊界一起使用:List<@Localized ? extends Message>,List<? extends @Localized Message>
  • 與方法和構造器引用一起使用:@Localized Message::getText

以下的類型位置是不能被註解的:

@NonNull String.class
import java.lang.@NonNull String;

注意事項:註解的作者需要指定特定的註解可以出現在哪裏。如果一個註解可以同時應用於變量和類型用法,並且它確實被應用到了某個變量聲明上,那麼該變量和類型用法就都被註解了。例如,public User getUser(@NonNull String userId),如果@NonNull可以同時應用於參數和類型用法,那麼uesrId參數就被註解了,而其參數類型是@NonNull String。

標準註解

Java SE在java.lang、java.lang.annotation和javax.annotation包中定義了大量的註解接口。其中四個是元註解,用於描述註解接口的行爲屬性,其他的三個是規則接口,可以用它們來註解你的源代碼中的項。下表中列出了這些註解,後文中將會將這些內容進行詳細的介紹。

註解接口 應用場合 目的
Deprecated 全部 將項標記爲過時的
SuppressWarnnings 除了包和註解之外的所有情況 阻止某個給定類型的警告信息
SafeVarargs 方法和構造器 斷言varargs參數可安全使用
Override 方法 檢查該方法是否重寫了某一個超類方法
FunctionalInterface 接口 將接口標記爲只有一個抽象方法的函數式接口
PostConstruct PreDestroy 方法 被標記的方法應該在構造之後或移除之前立即被調用
Resource 類、接口、方法、域 在類或接口上:標記爲其他地方要用到的資源。在方法或域上:爲“注入”而標記
Resources 類、接口 一個資源數組
Generated 全部
Target 註解 指明可以應用這個註解的那些項
Retention 註解 指明這個註解可以保留多久
Documented 註解 指明這個註解應該包含在註解項的文檔中
Inherited 註解 指明當這個註解應用於一個類的時候,能過自動被它的子類繼承
Repeatable 註解 指明這個註解可以在同一個項上應用多次

用於編譯的註解

  • @Deprecated註解可以被添加到任何不再鼓勵使用的項上。所以,當你使用一個已過時的項時,編譯器將會發出警告。這個註解與Javadoc標籤@deprecated具有同等功效。但是,該註解會一直持久化到運行時。

  • @SuppressWarnnings註解會告知編譯器阻止特定類型的警告信息,例如:@SuppressWarnnings("unchecked")

  • @Override這種註解只能用於方法上。編譯器會檢查具有這種註解的方法是否真正重寫一個來自於父類/超類的方法。例如:

    public class Test{
        @Override
        public int toString(int a){...};
        ...
    }
    

    這樣編譯器就會報告一個錯誤。因爲這個toString方法沒有重寫父類Object類的toString方法。

  • @Generated註解的目的是供代碼生成工具來使用。任何生成的源代碼都可以被註解,從而與程序員提供的代碼區分開。例如,代碼編輯器可以隱藏生成的代碼,或者代碼生成器可以移除生成代碼的舊版本。每個註解都必須包含一個表示代碼生成器的唯一標識符,而日期字符串和註釋字符串是可選的。

用於管理資源的註解

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

  • @Resource註解用於資源注入。例如,訪問數據庫的Web應用。數據庫訪問信息不應該被硬編碼到Web應用中。而是應該讓Web容器提供某種用戶接口,以便設置連接參數和數據庫資源的JNDI名字。在這個Web應用中,可以像下面這樣應用數據源:

    @Resource(name:"jdbc/mydb")
    private DataSource source;
    

    當包含這個域的對象被構造時,容器會“注入”一個對該數據源的引用。

元註解(自定義註解時需要使用的)

  • @Target元註解可以應用於一個註解,以限制該註解可以應用到哪些項上。例如:
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface Test

下表是@Target註解所有可能的取值情況,它們屬於ElementType枚舉類。可以指定任意數量的元素類型,用括號括起來。

元素類型 註解適用場合
ANNOTATION_TYPE 註解類型聲明
PACKAGE
TYPE 類(包括enum)及接口(包含註解類型)
METHOD 方法
CONSTRUCTOR 構造器
FIELD 成員域(包含enum常量)
PARAMETER 方法或構造器參數
LOCAL_VARIABLE 局部變量
TYPE_PARAMETER 類型參數
TYPE_USE 類型用法

一條沒有@Target限制的註解可以應用於任何項上。編譯器將檢查你是否將一條註解只應用到了某個允許的項上。例如,如果將@Test應用於一個成員域上,則會導致一個編譯器錯誤。

  • @Retention元註解用於指定一條註解應該保留多長時間。只能將其指定爲下表中的任意值,其默認值是RetentionPolicy.CLASS。
保留規則 描述
RetentionPolicy.SOURCE 不包括在類文件的註解
RetentionPolicy.CLASS 包括在類文件的註解,但是虛擬機不需要將它們載入
RetentionPolicy.RUNTIME 包括在類文件的註解,並由虛擬機載入。通過反射API可獲得它們
  • Documented元註解爲像Javadoc這樣的歸檔工具提供了一些提示。應該像處理其他修飾符一樣來處理歸檔註解,以實現其歸檔目的。其他註解的使用並不會納入歸檔的範疇。

總結

以上就是常用的註解類的使用,需要了解別的類的使用可以翻閱API文檔或者相關書籍。

反射

Java反射機制是Java語言一個很重要的特性,是Java"動態性"的重要體現。反射機制可以讓程序在運行時加載編譯期完全未知的類,使設計的程序更加靈活、開放。但是,反射機制的不足之處會大大降低程序運行的效率。

在實際開發中,直接使用反射機制的情況並不多,但是很多框架底層都會用到。爲此,理解反射機制會與更加深入的學習非常重要。

動態語言

動態語言是指在程序運行時,可以改變程序結構或變量的類型。典型的動態語言有Python、Ruby、JavaScript等。動態語言可以使得在執行的時候就完全改變了源碼的結構。這種動態性,可以讓程序更加靈活,更加具有開放性。

Java語言雖然具有動態性,但並不是動態語言。我們可以利用反射機制或字節碼操作獲得類似動態語言的特性。

反射機制的本質和Class類

學習反射機制基本就等同於學習Class類的用法。理解了Class類也就理解了反射機制。

Java反射機制讓我們在程序運行狀態中,對於任意一個類,都能知道該類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意一個方法。這種動態獲取以及動態調用對象方法的功能就是“Java的反射機制”。

反射機制的本質

反射機制是Java動態性的重要表現,但是反射機制也有缺點,那就是效率問題。反射機制會大大降低程序的執行效率。由於反射機制繞過了源代碼,也會給代碼維護增加困難。

Java在加載任何一個類時都會在方法區中建立“這個類對應的Class對象”,由於“Class對象”包含了這個類的整個結構信息,所以可以通過這個”Class對象“來操作這個類。

在使用一個類之前先要加載它,在加載完類之後,會在堆內存中產生了一個Class類型的對象(一個類只有一個Class對象),這個對象包含了完整的類的結構信息,可以通過這個對象知道類的結構。這個對象就像一面鏡子,透過它可以看到類的結構,因此被形象稱之爲反射。“Class對象”是反射機制的核心。

例如Class c = Class.forName(“cn.ac.whz.test.Student”);,Class.forName()可以讓程序員決定在程序運行時加載什麼樣的類,字符串傳入什麼類,程序就加載什麼類,完全和源代碼無關,這就是“動態性”。反射機制的應用實現了“運行時加載,探知與使用編譯期間完全未知的類”的可能。

反射機制的核心是“Class對象”。獲得了Class對象,就相當於獲得了類結構。通過“Class對象”可以調用該類的所有屬性、方法和構造器,這樣就可以動態加載與運行相關的類。

java.lang.Class類

java.lang.Class類是實現反射的根源。針對任何想動態加載、運行的類,唯有先獲得相應的Class對象。java.lang.Class類十分特殊,它用於表示Java中的類型(class,interface,enum,annotation,primitive type,void)本身。

Class類的對象可以用以下方法獲取:

  1. 運用getClass()。
  2. 運用.class語法。
  3. 運用Class.forName(),這是最常用的方法

以下是這三種方式的代碼示例:

package cn.whz.reflection;

/**
 * 測試各種類型對應的java.lang.Class對象的獲取方式
 * (class.interface,enum,annotation,primitive,type,void)
 * @author eddie
 *
 */
public class Demo01 {
	public static void main(String[] args) {
		String path="cn.whz.bean.User";
		try {
			Class clz1=Class.forName(path);
			//對象是表示或封裝一些數據。一個類被加載後,類的整個結構信息會放到對應的Class對象中
			//這個Class對象就像一面鏡子一樣,通過這面鏡子可以看到對應類的全部信息
			System.out.println(clz1.hashCode());
			
			Class clz2=Class.forName(path);  //一個類只對應一個Class對象
			System.out.println(clz2.hashCode());
			
			Class strClz1=String.class;
			Class strClz2=path.getClass();
			System.out.println(strClz1==strClz2);
			
			Class intClz=int.class;
			
			int[] arr1=new int[10];
			int[][] arr2=new int[10][3];
			int[] arr3=new int[30];
			double[] arr4=new double[10];
			//由於系統針對每個類只會創建一個Class對象,因此arr1和arr3指向的就是同一個對象
			System.out.println(arr1.getClass().hashCode());
			System.out.println(arr2.getClass().hashCode());
			System.out.println(arr3.getClass().hashCode());
			System.out.println(arr4.getClass().hashCode());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

反射機制的常見操作

反射機制的常見操作,實際上就是“Class對象”常用方法的應用,一般有如下幾種常見操作。

  1. 動態加載類、動態獲取類的信息(屬性、方法、構造器)
  2. 動態構造對象
  3. 動態調用類和對象的任意方法
  4. 動態調用和處理屬性
  5. 獲取泛型信息
  6. 處理註解

其中幾種操作中常用的類如下表所示:

類名 類的作用
Class類 代表類的構造信息
Method類 代表方法的結構信息
Field類 代表屬性的結構信息
Constructor類 代表構造器的結構信息
Annotation類 代表註解的結構信息

爲了方便測試,首先我們先定義一個簡單的User類。

package cn.whz.bean;
public class User {
	
	private int id;
	private String name;
	private int age;
	
	public int getId() {
		return id;
	}
	public void setId(int id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public User(int id, String name, int age) {
		super();
		this.id = id;
		this.name = name;
		this.age = age;
	}
	//javabean必須要有無參的構造方法
	public User() {
	}
}

利用反射的API獲取類的信息(類的名字、屬性、方法、構造器)

package cn.whz.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Demo02 {
	public static void main(String[] args) {
		String path="cn.whz.bean.User";
		try {
			Class clz=Class.forName(path);
			//獲取類的名字
			System.out.println(clz.getName());  //獲得包名+類名:cn.whz.bean.User
			System.out.println(clz.getSimpleName());  //獲得類名:User
			//獲得屬性信息
//			Field[] fields=clz.getFields();  只能獲取public的Field
			Field[] fields=clz.getDeclaredFields();  //獲得所有的Field
			Field f=clz.getDeclaredField("id");
			for (Field field : fields) {
				System.out.println("屬性:"+field);
			}
			System.out.println(f);
			//獲得方法信息
//			Method[] methods=clz.getMethods();  只能獲取public的Method
			Method[] methods=clz.getDeclaredMethods();
			Method m1=clz.getDeclaredMethod("getId", null);
			//如果方法有參數,則必須傳遞參數類型對應的Class對象
			Method m2=clz.getDeclaredMethod("setId", int.class);
			for (Method method : methods) {
				System.out.println("方法:"+method);
			}
			//獲得構造器信息
//			Constructor[] constructors=clz.getConstructors();  只能獲取public的構造器
			Constructor[] constructors=clz.getDeclaredConstructors();
			Constructor c1=clz.getConstructor(null);
			Constructor c2=clz.getConstructor(int.class,String.class,int.class);
			System.out.println("c1:"+c1+"\tc2:"+c2);
			for (Constructor constructor : constructors) {
				System.out.println("構造器:"+constructor);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

通過反射API動態的操作:構造器、方法、屬性

package cn.whz.reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import cn.whz.bean.User;

public class Demo03 {
	public static void main(String[] args) {
		String path="cn.whz.bean.User";
		try {
			Class clz=Class.forName(path);
			
			//通過反射API調用構造方法,構造對象
			User u1=(User) clz.newInstance();
			System.out.println(u1);
			
			Constructor<User> c=clz.getDeclaredConstructor(int.class,String.class,int.class);
			User u2=c.newInstance(1001,"小王",21);
			System.out.println(u2.getName());
			
			//通過反射API調用普通方法
			User u3=(User) clz.newInstance();
//			u3.setAge(21);
			Method m=clz.getDeclaredMethod("setName", String.class);
			m.invoke(u3, "大王");  //u3.setName("大王");
			System.out.println(u3.getName());
			
			//通過反射API操作屬性
			User u4=(User) clz.newInstance();
			Field f=clz.getDeclaredField("name");
			f.setAccessible(true);  //這個屬性不需要做安全檢查了,可以直接訪問
			f.set(u4, "王一");  //通過反射直接寫屬性的值
			System.out.println(f.get(u4));  //通過反射直接讀屬性的值
			System.out.println(u4.getName());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

通過反射獲取泛型信息

package cn.whz.reflection;

import java.lang.reflect.Type;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.List;
import java.util.Map;

import cn.whz.bean.User;

public class Demo04 {
	public void test01(Map<String, User> map,List<User> list) {
		System.out.println("Demo04.test01()");
	}
	public Map<Integer, User> test02() {
		System.out.println("Demo04.test02()");
		return null;
	}
	
	public static void main(String[] args) {
		try {
			//獲取指定方法參數的泛型信息
			Method m1=Demo04.class.getMethod("test01", Map.class,List.class);
			Type[] t=m1.getGenericParameterTypes();
			for (Type paramType : t) {
				System.out.println("#"+paramType);
				if (paramType instanceof ParameterizedType) {
					Type[] gengricTypes=((ParameterizedType) paramType).getActualTypeArguments();
					for (Type gengricType : gengricTypes) {
						System.out.println("泛型類型:"+gengricType);
					}
				}
			}
			//獲得指定方法返回值的泛型信息
			Method m2=Demo04.class.getMethod("test02", null);
			Type returnType=m2.getGenericReturnType();
			if (returnType instanceof ParameterizedType) {
				Type[] gengricTypes=((ParameterizedType) returnType).getActualTypeArguments();
				for (Type gengricType : gengricTypes) {
					System.out.println("返回值的泛型信息:"+gengricType);
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

反射機制的效率問題

反射機制的缺點是會大大降低程序的執行效率,採用反射機制的Java程序要經過字節碼解析過程,將內存中的對象進行解析,包括類一些動態類型,而JVM無法對這些代碼進行優化,因此,反射操作的效率要比那些非反射操作低得多。

結語

在寫完這篇後,由於接下來準備開始着手畢設以及準備秋招,對於Java SE的內容就暫告一段落了,整系列的內容,完全足夠小白學習,並且足夠應對日常的需求。本系列內容缺少的大概大概就圖形界面、Swing這些無關緊要的內容,在秋招之後,如果有時間我會將JUC、斷言、日誌、腳本引擎、XML以及JDBC的內容補上,並且會再獨立的寫GOF23種設計模式和項目實戰的相關係列博文。

自此這個系列宣告結束,如果把這十幾篇博文內容全部看完,對於Java SE的內容基本可以說得上大概瞭解和掌握了,最後祝各位大佬早日年薪百萬。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章