註解(Annotation)是JDK 5.0 開始引入的新技術,其主要作用是:
Annotation的格式:註解是以@註解名
格式再程式碼中存在的,而且還可以新增一些參數值,例如@SuppressWarnings(value = "unchecked")
。
Annotation的應用場景:註解可以附加在package、class、method和field等上面,相當於給這些元素新增了額外的輔助資訊和說明資訊,然後我們可以再通過 反射機制 機製 程式設計實現對註解提供的數據源的存取和使用。
內建註解爲Java預先爲我們準備的一些最常用的註解:
@Override
:該註解定義在java.lang.Override
中,只適用於修飾方法,表示宣告的這個方法是重寫的父類別中的方法。@Deprecated
:該註解定義在java.lang.Deprecated
中,可以修飾方法、屬性、類,表示不鼓勵程式設計師再去適用這樣的元素,但並不代表不能使用;通常是因爲該元素很危險或者已經有比該元素更好的替代品,即過期。@SuppressWarnings
:該註解定義在java.lang.SuppressWarnings
中,用來抑制編譯時的警告資訊,並且該註解需要新增參數才能 纔能正確使用,這些參數是事先定義好了。如:@SuppressWarnings(value = "all")
;@SuppressWarnings(value = "unchecked")
;@SuppressWarnings(value = {"unchecked","deprecation"})
等等…元註解(meta-annotation)的作用是負責註解其他的註解,對自定義的註解進行說明和規定,即幫助完成其他註解的定義。元註解在java.lang.annotation
包中,Java中定義了4個標準的元註解:
@Target
:用於描述註解的使用範圍,即定義的該註解可以在什地方使用。@Retention
:表示需要在什麼級別儲存該註解資訊,用於描述註解的宣告週期(原始碼<編譯檔案<執行時)。@Document
:說明該註解將被包含在javadoc中。@Inherited
:說明子類可以繼承父類別中的該註解。如下就是內建註解@Deprecated
的原始碼,使用了多個元註解:
/**
* A program element annotated @Deprecated is one that programmers
* are discouraged from using, typically because it is dangerous,
* or because a better alternative exists. Compilers warn when a
* deprecated program element is used or overridden in non-deprecated code.
*
* @author Neal Gafter
* @since 1.5
* @jls 9.6.3.6 @Deprecated
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
註解的定義與我們對介面的定義非常相似,只需要在interface
關鍵字前面加上@
符號即可,即@interface
。完整格式爲:public @interface 註解名 { 定義註解內容 }
。並且註解同樣與介面一樣,可以宣告方法,但作用缺與介面大相徑庭:
default
關鍵字來爲參數設定預設值,一般經常使用空字串或者0作爲預設值。。如String value() default "Korin";
表示預設值爲korbin的字串;value
,這樣可以在使用註解時省略參數名,直接給值;如下即爲簡單宣告的一個自定義的註解:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "";
String[] names();
int id() default 0;
}
上文的註解本質上用來存放說明性資訊和輔助資訊的,除了Java自己的一些註解可以被編譯器識別,那我們自定義的註解該如何使用這些資訊呢?只宣告卻不用不是就沒意義了嗎?答案是結合Java的反射機制 機製就可以很好的通過使用註解來完成很多工作。
動態語言和靜態的語言的概念:
- 動態語言是一類在程式碼執行時還可以去改變其程式碼結構的語言,如在程式執行過程中,新的函數、物件和甚至程式碼都可以被引進,並且已有的函數還可以被刪除或者修改。簡單的說就是我們寫好的程式碼可以根據我們的需求,動態的去改變其結構和邏輯。現在一些主要的動態語言有:Object-C、C#、JavaScript、PHP和Python等。
- 靜態語言則與動態語言相反,爲了保證程式的安全性,在程式執行時結構不允許被改變,常見的靜態語言有:Java、C和C++等。
Java不是動態語言,但是Java又可以稱之爲「準動態語言」。正是因爲Java中 反射機制 機製 的存在,使得Java有一定的動態性,使得Java程式設計變得更加靈活:
反射(Reflection)是Java在一定程度上可以被視爲動態語言的關鍵,Java的反射機制 機製允許程式在執行期間藉助於反射API取得任何類的內部資訊,並能直接操作任意物件的內部屬性和方法。
在Java的JVM虛擬機器中,載入完程式中的每一個類後,會在堆記憶體的方法區中生成一個Class
型別的物件,Class
本身就是Java中的一個類(java.lang.Class
),並且每一個自定義的類只有一個Class物件;而且實際上每個Class物件對應其所對應的類的.class編譯後的檔案。這個Class物件包含了完整這個類在JVM中的完整數據結構,並提供了存取這些數據結構的介面。所以我們就可以通過這個Class物件對該類進行相關的操作。
下圖爲正常方式通過全類名建立類的範例化物件;而反射方式則是反過來的,通過範例化的物件名,得到該物件的類物件。這也正是反射機制 機製的命名由來。
Java反射機制 機製的優點和缺點:
反射機制 機製的關鍵是:Class
類;並且實際上,反射機制 機製正是後續我們學習和使用的各種Java框架的設計靈魂~
Java中有提供了多種方式可以獲取一個類的Class
類物件:
class
屬性獲取Class類物件。該種獲取Class類物件的方法最爲安全可靠,且程式效能最高。getClass()
方法獲取其自己類的Class類物件。Class
類的靜態方法forName()
來獲取該類的Class類物件。TYPE
獲取Class類物件,如Class clazz = Integer.TYPE;
。那麼Java中有哪些型別有Class類物件呢?
class
:外部類、成員內部類、靜態內部類、區域性內部類和匿名內部類。interface
:介面[]
:陣列enum
:列舉@interface
:註解(annotation)基本數據型別(primitive type)
void
Java中預設每個類都會一個class
屬性,該屬性儲存了該類的所有結構資訊。呼叫該屬性返回的是該類的Class
物件。
範例自定義User
類,並通過User類的class
屬性獲取Class物件,列印輸出全包類名:
public class Main {
public static void main(String[] args) {
Class<User> clazz = User.class;
System.out.println(clazz);
/*
執行結果:class top.korbin.reflection.User
*/
}
}
class User {
private Long id;
private String name;
public Long getId() {return id; }
public void setId(Long id) {this.id = id; }
public String getName() {return name; }
public void setName(String name) {this.name = name; }
}
Java中的Object
類中提供了public final native Class<?> getClass()
方法實現通過物件範例獲取其類物件;並且此方法會被所有子類繼承。事實上Object類正是Java中所有類的父類別,即使沒有顯式宣告編譯器也會預設加上,所以相當於類每個都有這個方法。
直接通過範例物件的getClass()
方法獲取其Class物件,列印輸出全包類名:
public class Main {
public static void main(String[] args) {
User user = new User();
Class<? extends User> clazz = user.getClass();
System.out.println(clazz);
/*
執行結果:class top.korbin.reflection.User
*/
}
}
Java反射機制 機製的關鍵類:Class
,該類提供了public static Class<?> forName(String className)
靜態方法,通過全類名作爲該方法的參數,獲得類物件。如果所傳字串參數不合法,則需要捕獲異常:
public class Main {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("top.korbin.reflection.User");
System.out.println(clazz);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
/*
執行結果:class top.korbin.reflection.User
*/
}
}
類載入器即負責類的載入,併爲之生成對應的Class類物件。雖然我們不需要關心類載入機制 機製,但是瞭解這個類載入機制 機製我們就能更好的理解程式的執行~
在瞭解類載入過程之前,我們還得先基本瞭解 Java虛擬機器中的記憶體結構:
什麼時候進行類載入?
一般來說,只有在第一次 主動呼叫 某個類時纔會去進行類載入,即只初始化一次。如果一個類有父類別,會先去載入其父類別,然後再載入其自身。JVM 規定了以下六種情況爲 主動呼叫:
- 一個類的範例被建立(new操作、反射、cloning,反序列化);
- 呼叫類的static方法;
- 使用或對類/介面的static屬性進行賦值時(這不包括final常數與在編譯期確定的常數表達式);
- 當呼叫 API 中的某些反射方法時;
- 子類被初始化;
- 被設定爲 JVM 啓動時的啓動類(具有main方法的類)。
其餘的情況皆爲 被動呼叫,即JVM會自動去載入,如final常數。
Java類在JVM虛擬機器的載入過程:
在瞭解了上述的基礎知識之後,對於類的載入過程,當程式主動使用一個類時,如果該類還未被載入到Java記憶體中,也就是滿足上述的主動呼叫和被動呼叫情況下時,JVM則會通過如下三個步驟來對該類進行初始化:
.class
檔案的位元組碼內容載入到JVM記憶體方法區中,並將類的數據轉換成方法區對應的執行時數據結構,然後生成一個代表這個類的java.lang.Class
物件。
上圖爲測試執行在不同地方輸出靜態成員變數的值,結合上述的類載入過程觀察成員變數值初始化的過程:
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println("在main主函數中直接輸出其num靜態成員變數的值:"+A.num);
System.out.println("=============================================");
new A();
System.out.println("在main主函數中建立A類物件後,再輸出其num靜態成員變數的值:"+A.num);
}
}
class A {
public static int num = 100;
static {
System.out.println("A類的靜態程式碼塊初始化");
System.out.println("A類的靜態程式碼塊中num靜態變數值爲:"+num);
System.out.println("A類的靜態程式碼塊中給num賦值300");
num = 300;
}
public A(){
System.out.println("A類的無參構造方法初始化");
System.out.println("A類的無參構造方法中num靜態變數值爲:"+num);
System.out.println("A類的無參構造方法中給num賦值500");
num = 500;
}
}
類載入器的作用是將類(Class)裝在進JVM記憶體。JVM規範定義瞭如下型別的類載入器:
jre/lib/rt.jar
下的類都是核心類。該類載入器無法直接獲取。jre/lib/ext
目錄下的jar包或者-D java.ext.dirs
指定目錄下的jar包裝入工作庫。java -classpath
或者-D java.class.path
所指目錄下的類與jar包裝入工作庫。是最常用的類載入器。public class Main {
public static void main(String[] args) throws ClassNotFoundException {
//獲取系統類載入器
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
//獲取系統類載入器的父類別:擴充套件類載入器
ClassLoader extensionClassLoader = systemClassLoader.getParent();
System.out.println(extensionClassLoader);
//獲取擴充套件類載入器的父類別:引導類載入器(獲取不到,爲null)
ClassLoader bootstrapClassLoader = extensionClassLoader.getParent();
System.out.println(bootstrapClassLoader);
//獲取當前類的類載入器
ClassLoader currentClassLoader = new Main().getClass().getClassLoader();
System.out.println(currentClassLoader);
//獲取JDK內建的Object類的類載入器
ClassLoader objectClassLoader = Class.forName("java.lang.Object").getClassLoader();
System.out.println(objectClassLoader);
}
}
序號 | 方法名 | 功能描述 |
---|---|---|
1 | getName() | 返回String形式的該類的名稱。 |
2 | newInstance() | 根據某個Class物件產生其對應類的範例,它呼叫的是此類的預設構造方法(沒有預設無參構造器會報錯) |
3 | getClassLoader() | 返回該Class物件對應的類的類載入器。 |
4 | getSuperClass() | 返回某子類所對應的直接父類別所對應的Class物件 |
5 | isArray() | 判定此Class物件所對應的是否是一個數組物件 |
6 | getComponentType() | 如果當前類表示一個數組,則返回表示該陣列元件的 Class 物件,否則返回 null。 |
7 | getConstructor(Class[]) | 返回當前 Class 物件表示的類的指定的公有構造子物件。 |
8 | getConstructors() | 返回當前 Class 物件表示的類的所有公有構造子物件陣列。 |
9 | getDeclaredConstructor(Class[]) | 返回當前 Class 物件表示的類的指定已說明的一個構造子物件。 |
10 | getDeclaredConstructors() | 返回當前 Class 物件表示的類的所有已說明的構造子物件陣列。 |
11 | getDeclaredField(String) | 返回當前 Class 物件表示的類或介面的指定已說明的一個域物件。 |
12 | getDeclaredFields() | 返回當前 Class 物件表示的類或介面的所有已說明的域物件陣列。 |
13 | getDeclaredMethod(String, Class[]) | 返回當前 Class 物件表示的類或介面的指定已說明的一個方法物件。 |
14 | getDeclaredMethods() | 返回 Class 物件表示的類或介面的所有已說明的方法陣列。 |
15 | getField(String) | 返回當前 Class 物件表示的類或介面的指定的公有成員域物件。 |
16 | getFields() | 返回當前 Class 物件表示的類或介面的所有可存取的公有域物件陣列。 |
17 | getInterfaces() | 返回當前物件表示的類或介面實現的介面。 |
18 | getMethod(String, Class[]) | 返回當前 Class 物件表示的類或介面的指定的公有成員方法物件。 |
19 | getMethods() | 返回當前 Class 物件表示的類或介面的所有公有成員方法物件陣列,包括已宣告的和從父類別繼承的方法。 |
20 | isInstance(Object) | 此方法是 Java 語言 instanceof 操作的動態等價方法。 |
21 | isInterface() | 判定指定的 Class 物件是否表示一個介面型別 |
22 | isPrimitive() | 判定指定的 Class 物件是否表示一個 Java 的基本類型。 |
23 | newInstance() | 建立類的新範例 |
有了Class
類物件,我們就可以通過呼叫上問中的Class物件的方法,動態的獲取一個類的完整類結構(成員變數和方法等):
public class Main {
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
//模擬獲取到了一個向上轉型的Object物件
Object object = new User(10007L,"Korbin");
Class clazz = object.getClass();
//獲取類的結構,請自行列印輸出檢視
System.out.println(clazz.getName());//獲取全類名
System.out.println(clazz.getSimpleName());//獲取單獨的類名
Field[] fields = clazz.getFields();//獲取類的所有pulic公有屬性
Field[] declaredFields = clazz.getDeclaredFields();//獲取類的所有屬性,包括public和private屬性
Field name = clazz.getField("name");//通過指定的屬性名獲取屬性的物件
Method[] methods = clazz.getMethods();//獲取當前類及其父類別的全部public方法
Method[] declaredMethods = clazz.getDeclaredMethods();//獲取當前類的所有方法,包括public和private方法
Method getName = clazz.getMethod("getName", null);//通過指定的方法名和方法參數獲取方法的物件
Constructor[] constructors = clazz.getConstructors();//獲取類的全部public構造方法
Constructor[] declaredConstructors = clazz.getDeclaredConstructors();//獲取類的全部構造方法,包括private和public構造方法
Constructor constructor = clazz.getConstructor(Long.class, String.class);//通過構造參數獲取指定構造參數
}
}
既然有了Class
類物件,我們還可以直接通過newInstance()
方法動態的建立該類的物件範例,但需要滿足如下2個條件:
private
。當然,沒有無參構造器的類同樣可以建立物件範例。只需要先通過Class
類物件呼叫上文中介紹的構造方法getConstructor(Class ... parameterTypes)
,傳入構造器參數型別,然後通過該構造器Constructor
物件呼叫newInstance
方法傳入參數值後拿到類的範例化的物件。
public class Main {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class clazz = User.class;//也可以根據Class.forName獲取Class物件
Object user = clazz.newInstance();
System.out.println(user);
Constructor constructor = clazz.getConstructor(Long.class, String.class);
User user1 = (User) constructor.newInstance(1007L,"Korbin");
System.out.println(user1);
/*執行結果:
User{id=null, name='null'}
User{id=1007, name='Korbin'}
*/
}
}
上述通過反射機制 機製動態拿到了類的物件範例後,我們就可以進一步呼叫執行該範例物件的方法了。通過上文介紹的Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);
方法拿到Method
物件,然後呼叫其invoke();
方法,並傳入類方法的參數值(範例物件和範例物件方法的參數值),實現對該方法的呼叫:
public class Main {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
Class clazz = User.class;
User user = (User) clazz.newInstance();
System.out.println(user);
Method setNameMethod = clazz.getDeclaredMethod("setName", String.class);
setNameMethod.invoke(user,"Korbin111");
System.out.println(user);
/*
執行結果:
User{id=null, name='null'}
User{id=null, name='Korbin111'}
*/
}
}
注意,如果呼叫的方法是private
私有方法,將沒有足夠的許可權執行方法而拋出IllegalAccessException
異常,但是我們可以通過在執行方法前先呼叫Method
的setAccessible(true)
方法關閉存取安全檢查的開關。
反射拿到類的範例物件後,還可以通過上文介紹的Field name = clazz.getDeclaredField("name");
方法對類的屬性(成員變數)進行操作。如通過Field
的set()
方法傳入範例物件和屬性值作爲參數即可實現對屬性的賦值;通過get()
方法傳入屬性名作爲參數可以實現獲得屬性值:
public class Main {
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchFieldException {
Class clazz = User.class;
Object user = clazz.newInstance();
System.out.println(user);
Field name = clazz.getDeclaredField("name");
name.setAccessible(true);//關閉存取安全檢查開關
name.set(user,"Korbin666");
System.out.println(name.get(user));
System.out.println(user);
/*
執行結果:
User{id=null, name='null'}
Korbin666
User{id=null, name='Korbin666'}
*/
}
}
public class Main {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
test1();
test2();
test3();
}
//普通方式呼叫物件的方法
public static void test1(){
User user = new User();
long startTime = System.currentTimeMillis();
//執行10億次呼叫物件方法的操作
for (int i = 0; i < 1000000000; i++) {
user.getName();
}
long endTime = System.currentTimeMillis();
System.out.println("普通方式執行10億次時間:"+(endTime-startTime)+"ms");
}
//反射方式呼叫
public static void test2() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Object user = new User();
Class clazz = user.getClass();
Method getName = clazz.getDeclaredMethod("getName", null);
long startTime = System.currentTimeMillis();
//執行10億次呼叫物件方法的操作
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user,null);
}
long endTime = System.currentTimeMillis();
System.out.println("反射方式執行10億次時間:"+(endTime-startTime)+"ms");
}
//反射方式並關閉安全監測情況下呼叫
public static void test3() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
Object user = new User();
Class clazz = user.getClass();
Method getName = clazz.getDeclaredMethod("getName", null);
getName.setAccessible(true);
long startTime = System.currentTimeMillis();
//執行10億次呼叫物件方法的操作
for (int i = 0; i < 1000000000; i++) {
getName.invoke(user,null);
}
long endTime = System.currentTimeMillis();
System.out.println("反射並關閉安全監測方式執行10億次時間:"+(endTime-startTime)+"ms");
}
}
執行結果:
如果我們反射呼叫非常多,建議關閉存取安全檢查開關,提高程式執行效率~
Java中的泛型採用泛型擦除的機制 機製來引入泛型,Java中的泛型僅僅是給編譯器javac使用的,以確保數據的安全性和免去強制型別轉換所帶來的問題。所以,一旦編譯完成,所有和泛型有關的型別將全部操作。
爲了通過反射來操作泛型,Java新增了ParamterizedType
,GenericArrayType
,TypeVariable
和WildcardType
幾種型別來代表不能被歸爲Class
型別,但是又和原始型別齊名的型別。
ParamterizedType
:表示一種參數化型別,比如Collection<String>
;GenericArrayType
:表示一種元素型別是參數化型別或者型別變數的陣列型別;TypeVariable
:是各種型別變數的公共父介面;WildcardType
:代表一種萬用字元型別表達式。範例通過反射機制 機製獲取泛型:
public class Main {
public static void main(String[] args) throws NoSuchMethodException {
Method test1 = Main.class.getMethod("test1", Map.class, List.class);
//獲得方法的參數型別
Type[] genericParameterTypes = test1.getGenericParameterTypes();
for (Type genericParameterType: genericParameterTypes){
System.out.println(genericParameterType);
//獲取參數型別的結構化參數型別,即<>中的型別
if (genericParameterType instanceof ParameterizedType){
Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments){
System.out.println(actualTypeArgument);
}
}
}
System.out.println("=======================================================");
Method test2 = Main.class.getMethod("test2", null);
//獲得返回值型別
Type genericReturnType = test2.getGenericReturnType();
if (genericReturnType instanceof ParameterizedType){
Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
for (Type actualTypeArgument : actualTypeArguments){
System.out.println(actualTypeArgument);
}
}
}
public void test1(Map<String,User> map, List<User> list){
System.out.println("test1");
}
public Map<String,User> test2(){
System.out.println("test2");
return null;
}
}
首先,瞭解一下什麼是 ORM, 即Object Reletionship Mapping,也就是物件關係對映表。如下圖:
那麼我們如何通過反射機制 機製和註解搭配來完成一個類和表結構的對映關係呢?
public class Main {
public static void main(String[] args) throws NoSuchMethodException, NoSuchFieldException {
Class clazz = User.class;
//通過反射獲得類的註解
Annotation[] annotations = clazz.getAnnotations();
//獲得指定的註解
Table table = (Table)clazz.getAnnotation(Table.class);
System.out.println(table);
//獲取指定註解的值
System.out.println("Table註解的value參數值:"+table.value());
//獲取類屬性的註解
Field id = clazz.getDeclaredField("id");
TableField tableField = id.getAnnotation(TableField.class);
System.out.println(tableField);
System.out.println("TableField註解的name參數值:"+tableField.name());
System.out.println("TableField註解的type參數值:"+tableField.type());
System.out.println("TableField註解的length參數值:"+tableField.length());
}
}
@Table("user")
class User {
@TableField(name = "id", type = "Long",length = 10)
public Long id;
@TableField(name = "name", type = "String",length = 8)
public String name;
}
/**
* 類名的註解
* @author Korbin
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Table{
String value();
}
/**
* 屬性註解
* @author Korbin
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface TableField{
String name();
String type();
int length() default 1;
}