註解和反射機制使用特別簡單,但是它們在框架中被大量的使用,而如何靈活運用,想要深入理解框架,牢牢的掌握註解和反射機制的知識就顯得極其的重要了。
註解
註解不同於註釋,註釋僅只用於寫在源代碼中,來使自己或者別人更容易的翻閱源代碼。
註解是那些插入到源代碼中使用其他工具可以對其進行處理的標籤。這些工具可以在源代碼層次上進行操作,或者可以處理編譯器在其中放置了註解的類文件。
註解不會改變程序的編譯方式。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)將使用這個新的默認值,甚至在那些在默認值修改之前就已經編譯過的類文件中也是如此。
註解格式簡化
有兩個特殊的快捷方式可以用來簡化註解。
- 如果沒有指定元素,要麼是因爲註解中沒有任何元素,要麼是因爲所有元素都使用默認值,那麼你就不需要使用圓括號了。例如
@Test
和這個註解是一樣的@Test(id=0,name="小王",age=21)
,這樣的註解又稱爲標記註解。 - 另外一種快捷方式是
單值註解
。如果一個元素具有特殊的名字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類的對象可以用以下方法獲取:
- 運用getClass()。
- 運用.class語法。
- 運用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對象”常用方法的應用,一般有如下幾種常見操作。
- 動態加載類、動態獲取類的信息(屬性、方法、構造器)
- 動態構造對象
- 動態調用類和對象的任意方法
- 動態調用和處理屬性
- 獲取泛型信息
- 處理註解
其中幾種操作中常用的類如下表所示:
類名 | 類的作用 |
---|---|
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的內容基本可以說得上大概瞭解和掌握了,最後祝各位大佬早日年薪百萬。