Java反射與註解
主要是指程式可以訪問,檢測和修改它本身狀態或行為的一種能力,並能根據自身行為的狀態和結果,調整或修改應用所描述行為的狀態和相關的語義。
反射機制是什麼
面試有可能會問到,這句話不管你能不能理解,但是你只要記住就可以了
反射機制就是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為java語言的反射機制。
用一句話總結就是反射可以實現在執行時可以知道任意一個類的屬性和方法。
反射機制能做什麼
反射機制主要提供了以下功能:
- 在執行時判斷任意一個物件所屬的類;
- 在執行時構造任意一個類的物件;
- 在執行時判斷任意一個類所具有的成員變數和方法;
- 在執行時呼叫任意一個物件的方法;
- 生成動態代理(ps:這個知識點也很重要,後續會為大家講到)
Java 反射機制的應用場景
- 逆向程式碼 ,例如反編譯
- 與註解相結合的框架 例如Retrofit
- 單純的反射機制應用框架 例如EventBus
- 動態生成類框架 例如Gson
反射機制的優點與缺點
為什麼要用反射機制?直接建立物件不就可以了嗎,這就涉及到了動態與靜態的概念
靜態編譯:在編譯時確定型別,繫結物件,即通過。
動態編譯:執行時確定型別,繫結物件。動態編譯最大限度發揮了java的靈活性,體現了多型的應用,有以降低類之間的藕合性。
優點
- 可以實現動態建立物件和編譯,體現出很大的靈活性,特別是在J2EE的開發中它的靈活性就表現的十分明顯。比如,一個大型的軟體,不可能一次就把把它設計的很完美,當這個程式編譯後,釋出了,當發現需要更新某些功能時,我們不可能要使用者把以前的解除安裝,再重新安裝新的版本,假如這樣的話,這個軟體肯定是沒有多少人用的。採用靜態的話,需要把整個程式重新編譯一次才可以實現功能的更新,而採用反射機制的話,它就可以不用解除安裝,只需要在執行時才動態的建立和編譯,就可以實現該功能。
缺點
- 對效能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麼並且它滿足我們的要求。這類操作總是慢於只直接執行相同的操作。
理解Class類和類型別
想要了解反射首先理解一下Class類,它是反射實現的基礎。
類是java.lang.Class類的例項物件,而Class是所有類的類(There is a class named Class)
對於普通的物件,我們一般都會這樣建立和表示:
1 | Code code1 = new Code(); |
上面說了,所有的類都是Class的物件,那麼如何表示呢,可不可以通過如下方式呢:
1 | Class c = new Class(); |
但是我們檢視Class的原始碼時,是這樣寫的:
1 | private Class(ClassLoader loader) { |
可以看到構造器是私有的,只有JVM可以建立Class的物件,因此不可以像普通類一樣new一個Class物件,雖然我們不能new一個Class物件,但是卻可以通過已有的類得到一個Class物件,共有三種方式,如下:
1 | Class c1 = Code.class; 這說明任何一個類都有一個隱含的靜態成員變數class,這種方式是通過獲取類的靜態成員變數class得到的 |
這裡,c1、c2、c3都是Class的物件,他們是完全一樣的,而且有個學名,叫做Code的類型別(class type)。
這裡就讓人奇怪了,前面不是說Code是Class的物件嗎,而c1、c2、c3也是Class的物件,那麼Code和c1、c2、c3不就一樣了嗎?為什麼還叫Code什麼類型別?這裡不要糾結於它們是否相同,只要理解類型別是幹什麼的就好了,顧名思義,類型別就是類的型別,也就是描述一個類是什麼,都有哪些東西,所以我們可以通過類型別知道一個類的屬性和方法,並且可以呼叫一個類的屬性和方法,這就是反射的基礎。
舉個簡單例子程式碼:
1 | public class ReflectDemo { |
執行結果:
1 | com.tengj.reflect.ReflectDemo |
Java反射相關操作
在這裡先看一下sun為我們提供了那些反射機制中的類:
java.lang.Class;
java.lang.reflect.Constructor; java.lang.reflect.Field;
java.lang.reflect.Method;
java.lang.reflect.Modifier;
前面我們知道了怎麼獲取Class,那麼我們可以通過這個Class幹什麼呢?
總結如下:
- 獲取成員方法Method
- 獲取成員變數Field
- 獲取建構函式Constructor
下面來具體介紹
獲取成員方法資訊
兩個引數分別是方法名和方法引數類的類型別列表。
1 | public Method getDeclaredMethod(String name, Class<?>... parameterTypes) // 得到該類所有的方法,不包括父類的 |
舉個例子:
例如類A有如下一個方法:
1 | public void fun(String name,int age) { |
現在知道A有一個物件a,那麼就可以通過:
1 | Class c = Class.forName("com.tengj.reflect.Person"); //先生成class |
完整程式碼如下:
1 | public class Person { |
執行結果:
我叫tengj,今年10歲
怎樣,是不是感覺很厲害,我們只要知道這個類的路徑全稱就能玩弄它於鼓掌之間。
有時候我們想獲取類中所有成員方法的資訊,要怎麼辦。可以通過以下幾步來實現:
1.獲取所有方法的陣列:
1 | Class c = Class.forName("com.tengj.reflect.Person"); |
2.然後迴圈這個陣列就得到每個方法了:
1 | for (Method method : methods) |
完整程式碼如下:
person類跟上面一樣,這裡以及後面就不貼出來了,只貼關鍵程式碼
1 | public class ReflectDemo { |
執行結果:
getName
setName
setAge
fun
fun
getAge
這裡如果把c.getDeclaredMethods();改成c.getMethods();執行結果如下,多了很多方法,以為把Object裡面的方法也打印出來了,因為Object是所有類的父類:
getName
setName
getAge
setAge
fun
fun
wait
wait
wait
equals
toString
hashCode
getClass
notify
notifyAll
獲取成員變數資訊
想一想成員變數中都包括什麼:成員變數型別+成員變數名
類的成員變數也是一個物件,它是java.lang.reflect.Field
的一個物件,所以我們通過java.lang.reflect.Field
裡面封裝的方法來獲取這些資訊。
單獨獲取某個成員變數,通過Class類的以下方法實現:
引數是成員變數的名字
1 | public Field getDeclaredField(String name) // 獲得該類自身宣告的所有變數,不包括其父類的變數 |
舉個例子:
例如一個類A有如下成員變數:
1 | private int n; |
如果A有一個物件a,那麼就可以這樣得到其成員變數:
1 | Class c = a.getClass(); |
完整程式碼如下:
1 | public class ReflectDemo { |
執行結果:
hello wrold
同樣,如果想要獲取所有成員變數的資訊,可以通過以下幾步
1.獲取所有成員變數的陣列:
1 | Field[] fields = c.getDeclaredFields(); |
2.遍歷變數陣列,獲得某個成員變數field
1 | for (Field field : fields) |
完整程式碼:
1 | public class ReflectDemo { |
執行結果:
name
age
msg
獲取建構函式
最後再想一想建構函式中都包括什麼:建構函式引數
同上,類的成建構函式也是一個物件,它是java.lang.reflect.Constructor
的一個物件,所以我們通過java.lang.reflect.Constructor
裡面封裝的方法來獲取這些資訊。
單獨獲取某個建構函式,通過Class
類的以下方法實現:
這個引數為建構函式引數類的類型別列表
1 | public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes) // 獲得該類所有的構造器,不包括其父類的構造器 |
舉個例子:
例如類A有如下一個建構函式:
1 | public A(String a, int b) { |
那麼就可以通過:
1 | Constructor constructor = a.getDeclaredConstructor(String.class, int.class); |
來獲取這個建構函式。
完整程式碼:
1 | public class ReflectDemo { |
執行結果:
tengj
注意:Class的newInstance方法,只能建立只包含無引數的建構函式的類,如果某類只有帶引數的建構函式,那麼就要使用另外一種方式:
1 | fromClass.getDeclaredConstructor(String.class).newInstance("tengj"); |
獲取所有的建構函式,可以通過以下步驟實現:
1.獲取該類的所有建構函式,放在一個數組中:
1 | Constructor[] constructors = c.getDeclaredConstructors(); |
2.遍歷建構函式陣列,獲得某個建構函式constructor
:
1 | for (Constructor constructor : constructors) |
完整程式碼:
1 | public class ReflectDemo { |
執行結果:
public com.tengj.reflect.Person()
public com.tengj.reflect.Person(java.lang.String)
其他方法
註解需要用到的
1 | Annotation[] annotations = (Annotation[]) class1.getAnnotations();//獲取class物件的所有註解 |
獲取class物件的資訊
1 | boolean isPrimitive = class1.isPrimitive();//判斷是否是基礎型別 |
通過反射了解集合泛型的本質
擴充套件的知識點,瞭解就可以了。後續會為大家寫一篇關於泛型的文章。
首先下結論:
Java中集合的泛型,是防止錯誤輸入的,只在編譯階段有效,繞過編譯到了執行期就無效了。
下面通過一個例項來驗證:
1 | /** |
執行結果:
list2的長度是:1
true
list2的長度是:2
思維導圖
有助於理解上述所講的知識點
JAVA註解
概念及作用
- 概念
- 註解即元資料,就是原始碼的元資料
- 註解在程式碼中新增資訊提供了一種形式化的方法,可以在後續中更方便的 使用這些資料
- Annotation是一種應用於類、方法、引數、變數、構造器及包宣告中的特殊修飾符。它是一種由JSR-175標準選擇用來描述元資料的一種工具。
- 作用
- 生成文件
- 跟蹤程式碼依賴性,實現替代配置檔案功能,減少配置。如Spring中的一些註解
- 在編譯時進行格式檢查,如@Override等
- 每當你建立描述符性質的類或者介面時,一旦其中包含重複性的工作,就可以考慮使用註解來簡化與自動化該過程。
什麼是java註解?
在java語法中,使用@
符號作為開頭,並在@後面緊跟註解名。被運用於類,介面,方法和欄位之上,例如:
1 |
|
這其中@Override就是註解。這個註解的作用也就是告訴編譯器,myMethod()方法覆寫了父類中的myMethod()方法。
java中內建的註解
java中有三個內建的註解:
- @Override:表示當前的方法定義將覆蓋超類中的方法,如果出現錯誤,編譯器就會報錯。
- @Deprecated:如果使用此註解,編譯器會出現警告資訊。
- @SuppressWarnings:忽略編譯器的警告資訊。
本文不在闡述三種內建註解的使用情節和方法,感興趣的請看這裡
元註解
自定義註解的時候用到的,也就是自定義註解的註解;(這句話我自己說的,不知道對不對)
元註解的作用就是負責註解其他註解。Java5.0
定義了4個標準的meta-annotation型別,它們被用來提供對其它 annotation型別作說明。
Java5.0
定義的4個元註解:
@Target
@Retention
@Documented
@Inherited
java8加了兩個新註解,後續我會講到。
這些型別和它們所支援的類在java.lang.annotation包中可以找到。
@Target
@Target說明了Annotation所修飾的物件範圍:Annotation可被用於 packages、types(類、介面、列舉、Annotation型別)、型別成員(方法、構造方法、成員變數、列舉值)、方法引數和本地變數(如迴圈變數、catch引數)。在Annotation型別的宣告中使用了target可更加明晰其修飾的目標。
作用:用於描述註解的使用範圍(即:被描述的註解可以用在什麼地方)
取值(ElementType)有:
型別 | 用途 |
---|---|
CONSTRUCTOR | 用於描述構造器 |
FIELD | 用於描述域 |
LOCAL_VARIABLE | 用於描述區域性變數 |
METHOD | 用於描述方法 |
PACKAGE | 用於描述包 |
PARAMETER | 用於描述引數 |
TYPE | 用於描述類、介面(包括註解型別) 或enum宣告 |
比如說這個註解表示只能在方法中使用:
1 | ({ElementType.METHOD}) |
@Retention
@Retention定義了該Annotation被保留的時間長短:某些Annotation僅出現在原始碼中,而被編譯器丟棄;而另一些卻被編譯在class檔案中;編譯在class檔案中的Annotation可能會被虛擬機器忽略,而另一些在class被裝載時將被讀取(請注意並不影響class的執行,因為Annotation與class在使用上是被分離的)。使用這個meta-Annotation可以對 Annotation的“生命週期”限制。
作用:表示需要在什麼級別儲存該註釋資訊,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效)
取值(RetentionPoicy)有:
型別 | 用途 | 說明 |
---|---|---|
SOURCE | 在原始檔中有效(即原始檔保留) | 僅出現在原始碼中,而被編譯器丟棄 |
CLASS | 在class檔案中有效(即class保留) | 被編譯在class檔案中 |
RUNTIME | 在執行時有效(即執行時保留) | 編譯在class檔案中 |
使用示例:
1 | /*** |
@Documented
@Documented用於描述其它型別的annotation應該被作為被標註的程式成員的公共API,因此可以被例如javadoc此類的工具文件化。Documented是一個標記註解,沒有成員。
作用:將註解包含在javadoc中
示例:
1 | java.lang.annotation.Documented |
@Inherited
- 是一個標記註解
- 闡述了某個被標註的型別是被繼承的
- 使用了@Inherited修飾的annotation型別被用於一個class,則這個annotation將被用於該class的子類
@Inherited annotation型別是被標註過的class的子類所繼承。類並不從實現的介面繼承annotation,方法不從它所過載的方法繼承annotation
- 當@Inherited annotation型別標註的annotation的Retention是RetentionPolicy.RUNTIME,則反射API增強了這種繼承性。如果我們使用java.lang.reflect去查詢一個@Inherited annotation型別的annotation時,反射程式碼檢查將展開工作:檢查class和其父類,直到發現指定的annotation型別被發現,或者到達類繼承結構的頂層。
作用:允許子類繼承父類中的註解
示例,這裡的MyParentClass 使用的註解標註了@Inherited,所以子類可以繼承這個註解資訊:
1 | java.lang.annotation.Inherited |
1 |
|
1 | public class MyChildClass extends MyParentClass { |
自定義註解
格式
1 | public 註解名{ |
註解引數的可支援資料型別:
- 所有基本資料型別(int,float,double,boolean,byte,char,long,short)
- String 型別
- Class型別
- enum型別
- Annotation型別
- 以上所有型別的陣列
規則
- 修飾符只能是public 或預設(default)
- 引數成員只能用基本型別byte,short,int,long,float,double,boolean八種基本型別和String,Enum,Class,annotations及這些型別的陣列
- 如果只有一個引數成員,最好將名稱設為”value”
- 註解元素必須有確定的值,可以在註解中定義預設值,也可以使用註解時指定,非基本型別的值不可為null,常使用空字串或0作預設值
- 在表現一個元素存在或缺失的狀態時,定義一下特殊值來表示,如空字串或負值
示例:
1 | /** |