第1章 類加載器(ClassLoader)
1.1概述(瞭解)
類加載器:
負責將.class文件加載到內存中,併爲之生成對應的Class對象,也就是字節碼文件對象。
問題:我們平時書寫在idea中的Java程序是如何運行的呢?
1)首先將 .java 源文件編譯爲class類文件;
2)編譯後的類文件是存在硬盤中的,那麼我們運行需要在內存中看到效果,那麼類文件是如何被加載到內存中的呢,就是jvm通過類加載器ClassLoader把硬盤中的class文件加載到內存中,這樣就可以使用這個類中的成員變量和方法了。而被加載到內存中這個class文件就會變成一個Class類的對象。
常見的類加載器有三種,每個類加載器負責加載不同位置的類:
1)Bootstrap 根類加載器;
2)ExtClassLoader 擴展類加載器;
3)AppClassLoader 系統/應用類加載器;
那麼這三種類加載器各有什麼作用或者有什麼區別呢?
他們三個加載的範圍是不一樣的。如下圖所示:
說明:
1)Bootstrap是最頂級的類加載器。它加載類文件不是我們自己書寫的,負責Java核心類的,比如System,String等。只有所有類加載到內存中,我們纔可以使用。
2)ExtClassLoader 擴展類加載器,加載的是擴展類的,我們是用不到的,都是jdk內部自己使用的。
3)AppClassLoader 系統/應用類加載器,是用來加載ClassPath 指定的所有jar或目錄,ClassPath表示存放類路徑的,我們如果不配置ClassPath,那麼就表示當前文件夾,在idea環境下的ClassPath是out目錄。在out目錄存放的都是我們書寫好的class文件,也就是說 AppClassLoader 類加載器是用來加載我們書寫的out目錄下的class文件。
需求:演示類加載器的父子關係。
代碼演示如下所示:
分析:如何獲取一個類的類加載器呢?
如果想獲得當前類的加載器,那麼首先必須獲得當前類的字節碼文件對象,而這個字節碼文件對象屬於Class類型,我們可以使用 Class類中的getClassLoader()函數來獲得類加載器:
ClassLoader getClassLoader() 返回該類的類加載器
AppClassLoader:加載classPath中的所有的類,也就是我們自己寫的那些類!
注意:類加載器,也是一個類,也需要被加載。一般類加載器都是被父類加載器加載的!
獲取父類加載器的方法:使用ClassLoader 類中的getParent()返回委託的父類加載器 。
說明:AppClassLoader是被ExtClassLoader加載的!
ExtClassLoader肯定也是一個類,需要被父加載,它的父親是BootStrap。
那麼問題來了:如果這個類加載器也需要被人加載,那麼就沒有盡頭了!因此,BootStrap是不需要被加載的。
因爲它不是一個Java類。它是用C++實現的一段代碼。
也就是說,jvm虛擬機一啓動就會運行C++實現的這段代碼,那麼BootStrap類一旦被啓動就會開始加載他下面的子類了。
注意:最頂級的類加載器不是Java類,而是C++實現的代碼。
/*
* 演示類加載器的父子關係:
* 獲取一個類加載器:使用Class類中的函數:ClassLoader getClassLoader() 返回該類的類加載器
* 獲取Class類的對象 :類名.class
* AppClassLoader:加載classPath中的所有的類,也就是我們自己寫的那些類!
* 類加載器,也是一個類,也需要被加載。一般類加載器都是被父類加載器加載的!
* 獲取父類加載器的方法:使用ClassLoader類中的函數:
* ClassLoader getParent()返回委託的父類加載器
*
* AppClassLoader類加載器是被ExtClassLoader類加載器加載的!
*
* ExtClassLoader肯定也是一個類,需要被父類加載器加載,它的父類是BootStrap類加載器。
* 而ExtClassLoader確實被他的父類BootStrap類加載器加載的,
* 那麼問題來了:如果BootStrap類加載器也需要被人加載,那麼就沒有盡頭了!因此,BootStrap類加載器是不需要被加載的。
* 因爲它不是一個Java類。它是用C++語言實現的一段代碼。
* 所以這裏獲取不到BootStrap類加載器,就是因爲他是一段C++代碼實現的。
*/
public class ClassLoaderDemo1 {
public static void main(String[] args) {
// 獲取當前類的加載器
ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
//輸出當前類的類加載器
System.out.println(loader);//sun.misc.Launcher$AppClassLoader@b0014f0
//獲取AppClassLoader類加載器的父類
ClassLoader parent = loader.getParent();
//輸出AppClassLoader類加載器的父類加載器
System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@325e9e34
//獲取ExtClassLoader類加載器的父類
ClassLoader grandpa = parent.getParent();
//輸出ExtClassLoader類加載器的父類加載器
System.out.println(grandpa);//null
}
}
注意:
通過上述學習我們發現三種類加載器都有自己要加載的類文件,各司其職,不能亂加載,比如Bootstrap類加載器只能加載sun公司定義好的類,他就不能加載自定義的類文件。所有的自定義類文件都是由AppClassLoader類加載器加載。
1.2委託機制(雙親委派機制)(瞭解)
通過上述學習我們發現三種類加載器都有自己要加載的類文件,各司其職,不能亂加載,比如Bootstrap類加載器只能加載JRE/lib/rt.jar 包下的類,他就不能加載JRE/lib/ext/*.jar包下的類文件,我們把上述這種關係叫做全盤負責委託機制(雙親委派機制)。
全盤負責委託機制:
ClassLoader(類加載器)加載類用的是全盤負責委託機制。
**1)全盤負責:**當一個ClassLoader(類加載器)加載一個類的時候,那麼在這個類中所引用的所有其它的類通常也都由這個類加載器來加載。
舉例:比如上述代碼中我們在 ClassLoaderDemo1 類中書寫如下代碼:
public class ClassLoaderDemo1 {
public static void main(String[] args) {
// 獲取當前類的加載器
ClassLoader loader = ClassLoaderDemo1.class.getClassLoader();
//輸出當前類的類加載器
System.out.println(loader);//sun.misc.Launcher$AppClassLoader@b0014f0
//獲取AppClassLoader類加載器的父類
ClassLoader parent = loader.getParent();
//輸出AppClassLoader類加載器的父類加載器
System.out.println(parent);//sun.misc.Launcher$ExtClassLoader@325e9e34
}
}
說明:由於我們在ClassLoaderDemo1 類中使用了System類,那麼System類也應該由ClassLoaderDemo1的類加載器加載到內存中。
換句話說,如果在A類中使用了B類,那麼A類的加載器就會將B類也會加載到內存中,就是一個類的加載器同時把多個類都加載了。
2)委託機制:先讓Parent(父)類加載器尋找。
但是呢,全盤負責要和委託機制一起使用,一個類加載器在加載一個類的時候不是上來就先加載類,而是先諮詢這個類加載器的父親,先看他的父類加載器有沒有要加載的類,如果已經存在要加載的類了,那麼子類加載器就不會加載,因爲在加載就會重複,產生衝突了,只有在父類加載器中找不到的時候,才從自己的範圍中尋找。
舉例:還是上述的代碼,由於ClassLoaderDemo1 類是被 AppClassLoader 類加載器加載內存中的,那麼根據全盤負責機制,AppClassLoader 類加載器也會將System類加載到內存中,但是在加載的時候,根據委託機制AppClassLoader 類加載器會先去諮詢他的父親ExtClassLoader 類加載器,而這個類加載器中也沒有System類,那麼又會去諮詢ExtClassLoader 類加載器的父類Bootstrap類加載器,而在這個類加載器中是可以加載System類的,所以作爲子類加載器AppClassLoader 就不會加載了,這樣才能保證一個類只會被加載一次,任何一個類同時只會被加載一次。
如果一個類在父類加載器中找到了,那麼就會把這個類加載之後保存到cache(緩存)中。
3)類加載器的cache(緩存)機制:如果cache中保存了這個類就直接返回它,如果沒有才加載這個類,然後存入cache中,下一次如果有其他類在使用的時候就不會在加載了,直接去cache緩存拿即可。這就是爲什麼每個類只加載一次,內存只有一份的原因。
舉例:還是上述代碼中,當第一次使用System類的時候,那麼System類就會被加載了,那麼System類就會存儲到內存中了,當下面代碼中我們再一次使用System類的時候,由於內存中已經有了,那麼就不會在去加載了,這時會直接拿過來用即可。
因此方法區中每一個類的字節碼文件只有一份的原因由全盤負責、委託機制和類加載器的cache(緩存)機制共同決定。也稱爲雙親委派機制
注意:雙親委派機制具有低效的傳達機制,最後還是各司其職。所以從jdk9後對雙親委派機制已經破壞。纔有如下方式:
1.經過破壞後的雙親委派模型更加高效,減少了很多類加載器之間不必要的委派操作
2.JDK9的模塊化可以減少Java程序打包的體積,同時擁有更好的隔離性與封裝性
3.每個moudle擁有專屬的類加載器,程序在併發性上也會更加出色
總結:
1.全盤負責:就是在當前類中使用的其他類都是由當前類的類加載器加載。
2.委託機制:去詢問父類加載器是否可以加載,如果能加載則由父類加載器加載。不能則由本類的類加載器加載
3.cache緩存機制:如果一個類.class被加載到了內存中使用一次就會被放到cache緩存中,然後下次使用直接從cache緩存中拿即可
全盤負責 委託機制 cache緩存機制 共同決定了內存中永遠只有一份某個類的字節碼文件對象(類名.class).
舉例:內存只有一份System.class
1.3 使用類加載器加載配置文件的方式(掌握)
需求:用類加載器加載配置文件。
代碼步驟:
1)新建一個類ClassLoaderDemo2 ,並書寫main函數;
2)在當前項目下新建一個stu.ini文件,並在其中輸入:
name=zhangsan
age=19
3)創建Properties 類的集合對象p;
4)創建字節輸入流FileInputStream對象關聯項目根目錄文件stu.ini;
5)使用集合對象p調用load函數加載配置文件中的內容,並使用輸出語句輸出內容即可;
使用之前的FileInputStream方式加載配置文件方式加載文件內容:
//使用之前的加載配置文件方式加載文件內容
public void method_1() throws Exception{
// 創建Properties類的對象
Properties p = new Properties();
//調用函數加載配置文件中內容
p.load(new FileInputStream("stu.ini"));
//輸出內容
System.out.println(p);
}
說明:直接使用FileInputStream的相對路徑,是相對於項目。
注意:我們在開發中配置文件stu.ini不僅可以存放到項目的根目錄,有時我們還會存放到src下面:
這樣我們使用new FileInputStream(“stu.ini”)在讀取就會出現如下問題:
錯誤原因:new FileInputStream(“stu.ini”)是去項目的根目錄去查找stu.ini文件,而由於我們已經將該文件移動到src下面,所以會報找不到指定文件的異常。
既然使用之前的加載方式加載src下面的配置文件有問題,那麼我們可以使用新的加載方式,就是用類加載器加載。
分析步驟:
1)使用當前類ClassLoaderDemo2獲得Class對象並調用Class類中的getClassLoader()函數:
ClassLoader loader = ClassLoaderDemo2.class.getClassLoader();
2)使用類加載器對象loader 調用ClassLoader 類中的InputStream getResourceAsStream(String name)返回讀取指定資源的輸入流
InputStream in = loader.getResourceAsStream(“stu.ini”);
說明:這裏的name是文件的路徑:這個路徑如果使用相對路徑,相對的是src目錄。
代碼演示如下所示:
//使用之前的加載配置文件方式加載文件內容
public void method_1() throws Exception{
// 創建Properties類的對象
Properties p = new Properties();
//獲取當前類的加載器
ClassLoader loader = ClassLoaderDemo2.class.getClassLoader();
//使用類加載器對象調用函數獲取加載配置文件的字節輸入流
InputStream in = loader.getResourceAsStream("stu.ini");
//調用函數加載配置文件中內容
// p.load(new FileInputStream("stu.ini"));
p.load(in);
//輸出內容
System.out.println(p);
}
總結:這兩種加載文件的區別在於加載的相對位置不一樣,第一種方式相對的是當前項目,而第二種方式相對的是src。
第2章 反射-框架設計的靈魂
2.1 反射概述
問題:我們平時書寫在idea中的Java程序是如何運行的呢?
1)首先將 .java 源文件編譯爲class類文件;
2)編譯後的類文件是存在硬盤中的,那麼我們運行需要在內存中看到效果,那麼類文件是如何被加載到內存中的呢,就是jvm通過類加載器ClassLoader把硬盤中的class文件加載到內存中,這樣就可以使用這個類中的成員變量和方法了。而被加載到內存中這個class文件就會變成一個Class類的對象。
反射要依賴於Class類。
由於Class表示類文件的字節碼文件對象,類字節碼文件就是在描述一個類,描述類的成員變量、成員函數和構造函數。
而反射就是從一個類的字節碼文件中拿到成員變量、成員函數和構造函數。要想從一個類中拿東西必須拿到這個類的字節碼文件對象,所以反射依賴於Class,因此我們在學習反射之前先了解下Class。
2.1.1 Class類介紹
在Java中使用類來描述所有的事物,而這些描述完的所有程序,在編譯完之後統一都會生成各自class文件。
在Java中class文件是所有源代碼(.java 文件)程序編譯後統一的結果。class文件是一類可以被JVM直接執行的文件。class文件在Java世界中就是存在的一類事物。
Java使用Class類來描述和封裝class文件這類事物。class文件也叫作字節碼文件。
關於Class描述字節碼文件如下圖所示:
說明:
1)Java中使用Class類表示某個class文件;
2)任何一個class文件都是Class這個類的一個實例對象;
Class的API描述:
說明:
1)Class類它可以表示Java中的任何內容;
2)Java中使用Class類表示硬盤上的某個.class文件,啓動JVM就會把文件加載到內存中,佔用一片空間,稱爲一個字節碼文件對象,這個對象就是Class類的一個實例。不同的類,有自己的字節碼文件對象,這些對象都是Class類的實例;
3)說明:在Class類中專門提供了幾個獲取成員變量 、成員函數 、構造函數的函數。
Field getField(String name) 表示獲取類中的成員變量的對象;
Method getMethod() 表示獲取類中的成員函數的對象;
Constructor getConstructor() 表示獲取類中的構造函數的對象;
2.1.2 獲取Class的三種方式(掌握)
因爲反射技術是通過Class對象來實現把一個類進行解剖的,所以需要先了解怎麼樣纔可以獲取到Class對象。
需求:演示獲取Class的三種方式:
1)獲取Class對象的第一種方式:使用類的class屬性直接獲取:類名.class。
說明:在任何的一個類中都有一個靜態成員變量class,可以直接獲取到class文件所屬的Class對象。
2)獲取Class對象的第二種方式:在Object類中,有個getClass方法,就可以獲取到任何一個對象對應的Class對象。對象.getClass()。
3)獲取Class對象的第三種方式:在Class類中有個靜態的方法:static Class forName(String className),根據類的名稱獲取類的Class對象。
說明:這裏的參數className必須是類的全名(就是帶有包名的全名稱)。如:Class.forName(“java.lang.String”);
補充:上述三種方式可以獲得Class對象,獲得完Class對象就可以獲取類的基本信息:
獲取類的基本信息:
String getName() 獲取類的名稱,就是獲得包名.類名
String getSimpleName() 獲取類的簡稱 類名
代碼演示如下所示:
/*
* 演示:獲取Class的三種方式
* 方式1:類名.class
* 方式2:對象.getClass()
* 方式3:static Class forName(String className)根據類的名稱獲取類的Class對象
* 注意:這裏的className必須是類的全名!
*
* 獲取類的基本信息:
* String getName() 獲取類的名稱 包名.類名
* String getSimpleName() 獲取類的簡稱 類名
*/
public class ClassDemo1 {
public static void main(String[] args) throws ClassNotFoundException {
// 方式1:類名.class
Class clazz = String.class;
// 輸出clazz全名 就是包名.類名 java.lang.String
System.out.println(clazz.getName());
// 輸出clazz精簡名字 就是類名 String
System.out.println(clazz.getSimpleName());
// 方式2:對象.getClass()
Class clazz2 = "柳巖".getClass();
// 輸出clazz2全名 就是包名.類名 java.lang.String
System.out.println(clazz2.getName());
// 輸出clazz2精簡名字 就是類名 String
System.out.println(clazz2.getSimpleName());
// 方式3:static Class forName(String className)根據類的名稱獲取類的Class對象
// Class clazz3 = Class.forName("String");//這裏必須是類的全名 包名.類名,否則報java.lang.ClassNotFoundException
Class clazz3 = Class.forName("java.lang.String");
// 輸出clazz3全名 就是包名.類名 java.lang.String
System.out.println(clazz3.getName());
// 輸出clazz3精簡名字 就是類名 String
System.out.println(clazz3.getSimpleName());
//說明對於String類來說在內存中只有一個String.class文件
System.out.println(clazz==clazz2);//true
System.out.println(clazz==clazz3);//true
System.out.println(clazz2==clazz3);//true
}
}
總結:上述三種方式都是用來獲取Class對象的,那麼在開發中一般使用哪種獲取方式呢?
在開發中我們會使用第三種獲取方式。
說明:第三種的好處就是加載一個類卻不需要知道這個類是什麼,通過第一種方式獲取前提是必須得先知道類名,然後才能通過類名.class獲取。而第二種方式必須知道對象才能通過對象.getClass()來獲取。
而第三種不需要知道類名或者對象就可以直接獲取,或者可以這樣理解,我們在真實開發中,類的名字是不知道的,都是通過IO流來讀取配置文件讀取回來的。
也就是說我們讀取配置文件的時候根據key來獲取String類型的value,這樣就可以把String類型的value作爲forName(String className)函數的參數,而不需要在程序代碼中體現出類名字樣就可以獲得Class對象了。
2.1.3 預定義對象(瞭解)
通過查閱Class的API發現8種基本數據類型、void關鍵字和數組也表示爲Class的對象,我們把8種基本數據類型、void叫做預定義對象,一共有9種。
說明:
1)基本數據類型要保存在內存中,那麼他們也會有自己的字節碼,獲取的Class對象中保存的就是他們本身,因爲基本數據類型不屬於任何包下的。
如:Class c = int.class;對象c中保存的就是int。
2)對於函數返回值類型void,代表的是沒有返回值類型,那麼他也是一種數據類型,表示沒有類型,那麼輸出對象也是void本身。
如:Class c1 = void.class;對象c1中保存的就是void。
代碼演示如下:
/*
* 演示:9種預定義對象:
* 包含:8種基本數據類型以及void
*/
public class ClassDemo2 {
public static void main(String[] args) {
// 獲取int類型的Class對象
Class c = int.class;
System.out.println(c);// int
// 獲取void類型的Class對象
Class c1 = void.class;
System.out.println(c1);// void
}
}
總結:任何的一種數據都具有自己的字節碼。
2.1.4 反射的概念
到目前爲止我們已經瞭解瞭如何獲得Class,那麼接下來我們就會使用反射技術來剖析一個class文件。
那麼到底什麼是反射呢?
反射就是通過一個類的Class對象把類中的各種成員映射成對應的Java類。一個類中的:成員變量、構造函數、成員方法都有對應的Java類:Field、Contructor、Method; 就比如:一個汽車是一個大類,汽車中的發動機、輪胎等等都可以是一個個小的類。
一個類的Class對象可以獲取其所有成員的信息,比如一個方法的名稱、修飾符、參數類型、返回值等等信息封裝成一個描述方法的類(Method)中。
換句話說反射通過Class類的對象可以獲取一個類中的成員,比如函數,保存在Method類中。然後通過Method類的對象來獲取一個成員函數的名稱、修飾符、參數類型、返回值等等信息。
一個類中的所有成員,都可以通過Class對象獲得,並封裝爲對應的對象。我們拿到這些對象以後,有什麼用?怎麼用?這正是我們學習反射的要點!
2.2 使用反射獲取一個類中的構造函數
2.2.1使用Class類中的newInstance()函數創建某個類的對象
通過上述辦法已經獲取到了Class對象,即就是獲取到某個類的class文件。現在我們不使用Java中的new關鍵字來創建這個class文件所描述的那個事物的真實對象, 而通過Class這個對象,動態的創建這個類的真實對象。
在Class類中有一個newInstance()方法,可以動態的創建出當前這個class文件的真實對象。
該方法如下所示:
代碼演示如下所示:
Person類:
/*
* 描述人
*/
public class Person {
//屬性
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public Person() {
}
}
測試類:
/*
* 使用Class類中的newInstance()函數獲取某個class文件中的真實對象
*/
public class ReflectDemo1 {
public static void main(String[] args) throws Exception {
// 獲取Class類的對象
Class clazz = Class.forName("cn.itcast.sh.reflect.demo.Person");
//使用對象調用函數創建Person類的對象
Person p = (Person) clazz.newInstance();
/*
* 上述兩行代碼等同於
*Person p= new Person();
*注意:這裏是使用Person類中的無參構造函數在創建對象,所以要求Person類中必須具備無參構造函數,
*否則就會報 java.lang.InstantiationException:不能實例化異常
*/
System.out.println(p);//cn.itcast.sh.reflect.demo.Person@5a20d10a
//利用無參構造函數創建對象
String s = String.class.newInstance();
System.out.println(s.length());//0
}
}
注意:使用Class類中的newInstance()方法的時候,要求class文件中必須有空參數並且不能是private修飾的構造函數。如果沒有,那麼使用newInstance()方法就會報異常。
2.2.2 使用反射獲取一個類中的所有構造函數(包括有參數和私有的構造函數)(Constructor類)
說明:
之前,我們都是使用Class類中的newInstance()調用無參的構造函數來創建對象的,對於有參數的構造方法無法實現調用並創建這個類的對象,所以sun公司專門給我們提供瞭如何獲取一個類中的所有構造函數。
說明:
1)使用Class類的對象即字節碼對象可以獲取class文件中的所有構造函數,具體應該藉助Class類中的如下函數:
a:Constructor[] getConstructors() 獲取所有公共的構造函數
b:Constructor[] getDeclaredConstructors() 獲取所有構造函數 包括私有的
c:Constructor getConstructor(Class ... parameterTypes)根據參數列表獲取指定的公共的構造函數
說明:由於這裏需要Class類的對象,所以在給參數的時候,直接使用實參類型的類獲取Class對象即可。
舉例:假設需要獲取String類型的構造函數,那麼這裏直接使用String.class作爲getConstructor(Class … parameterTypes)的參數。
d:Constructor getDeclaredConstructor(Class ... parameterTypes)根據參數列表獲取指定的構造函數 包括私有的
2)獲取到構造函數對象之後,就可以使用獲取的構造函數對象創建某個類的真實對象。我們通過反射已經獲取到構造函數,查閱Constructor類中的描述,發現Constructor類中的newInstance(Object… initargs) 方法,這個方法可以動態的創建出這個構造函數對象所表示的那個類的真實對象。
說明: Object… initargs 創建對象的時候需要傳遞的真實的數據,就是構造函數所需要的實際參數。
代碼如下所示:
需求:獲取File類的構造函數,並使用獲取的構造函數創建對象。
D:\test\out.txt 作爲newInstance函數的參數,創建對象之後並獲取絕對路徑。
/*
* 演示:反射獲取構造函數。
* Constructor[] getConstructors() 獲取所有公共的構造函數
* Constructor[] getDeclaredConstructors() 獲取所有構造函數 包括私有的
* Constructor getConstructor(Class ... parameterTypes)根據參數列表獲取指定的公共的構造函數
* 說明:由於這裏需要Class類的對象,所以在給參數的時候,直接使用實參類型的類獲取Class對象即可
* 舉例:假設需要獲取String類型的構造函數,那麼這裏直接使用String.class作爲getConstructor(Class ... parameterTypes)的參數
* Constructor getDeclaredConstructor(Class ... parameterTypes)根據參數列表獲取指定的構造函數 包括私有的
*
* 獲取到構造函數對象之後,就可以使用獲取的構造函數對象創建某個類的真實對象。我們通過反射已經獲取到構造函數,
* 查閱Constructor類中的描述,發現Constructor類中的newInstance(Object... initargs) 方法,
* 說明: Object... initargs 創建對象的時候需要傳遞的 * 這個方法可以動態的創建出這個構造函數對象所表示的那個類的真實對象。
真實的數據,就是構造函數所需要的實際參數。
*/
public class ReflectDemo2 {
public static void main(String[] args) throws Exception {
// 獲取字節碼文件對象
Class clazz=File.class;
//Constructor[] getConstructors() 獲取所有公共的構造函數
Constructor[] cons = clazz.getConstructors();
for (Constructor con : cons) {
/*
* public java.io.File(java.lang.String,java.lang.String)
* public java.io.File(java.lang.String)
* public java.io.File(java.io.File,java.lang.String)
* public java.io.File(java.net.URI)
*/
System.out.println(con);
}
System.out.println("--------------");
//Constructor[] getDeclaredConstructors() 獲取所有構造函數
Constructor[] constructors = clazz.getDeclaredConstructors();
for (Constructor constructor : constructors) {
/*
* public java.io.File(java.lang.String,java.lang.String)
* public java.io.File(java.lang.String)
* private java.io.File(java.lang.String,java.io.File)
* public java.io.File(java.io.File,java.lang.String)
* public java.io.File(java.net.URI)
* private java.io.File(java.lang.String,int)
*/
System.out.println(constructor);
}
System.out.println("--------------");
//需求:我要獲取File(String pathname)
//Constructor getConstructor(Class ... parameterTypes)根據參數列表獲取指定的公共的構造函數
Constructor constructor = clazz.getConstructor(String.class);
//public java.io.File(java.lang.String)
System.out.println(constructor);
//需求:獲取private File(String child, File parent)
//Constructor getDeclaredConstructor(Class ... parameterTypes)根據參數列表獲取指定的構造函數 包括私有的
System.out.println("--------------");
//private java.io.File(java.lang.String,java.io.File)
Constructor constructor2 = clazz.getDeclaredConstructor(String.class,File.class);
System.out.println(constructor2);
//使用獲得的構造函數創建對象 newInstance(Object... initargs)
File f = (File) constructor.newInstance("D:\\test\\out.txt");
System.out.println(f.getAbsolutePath());//D:\test\out.txt
}
}
注意:通過Class類中的Constructor[] getDeclaredConstructors()和Constructor getDeclaredConstructor(Class … parameterTypes)函數可以獲得類中的所有的構造函數,包括私有的構造函數,但是私有的構造函數我們在其他類中是無法使用的,如果要想使用必須強制取消Java對私有成員的權限檢測或者可以理解暴力訪問。
需求:使用反射技術獲得File類中的私有構造函數 private File(String child, File parent) 並創建File類的對象獲得指定路徑的絕對路徑。
注:String child=”柳巖.jpg”,File parent=new File(“D:\test”)
代碼實現如下所示:
public static void main(String[] args) throws Exception {
// 獲取字節碼文件對象
Class clazz=File.class;
/*
* 需求:使用反射技術獲得File類中的私有構造函數 private File(String child, File parent)
* 並創建File類的對象獲得指定路徑的絕對路徑。
* 注:String child="柳巖.jpg",File parent=new File("D:\\test")
*/
Constructor constructor2 = clazz.getDeclaredConstructor(String.class,File.class);
//創建File類的對象 使用獲得的構造函數創建對象 newInstance(Object... initargs)
File f2 = (File) constructor2.newInstance("柳巖.jpg",new File("D:\\test"));
System.out.println(f2.getAbsolutePath());
}
上述代碼發生瞭如下異常:
IllegalAccesssException異常是在沒有訪問權限時,就會引該異常。
解決上述異常,我們必須強制取消Java對私有成員的權限檢測或者可以理解暴力訪問,需要使用Constructor的父類AccessibleObject類中的函數:
AccessibleObject類如下所示:
使用如下函數即可:
上述函數表示強制取消Java對私有成員的權限檢測。
或者可以理解暴力對私有成員進行訪問。
改進的代碼如下所示:
/*
* 需求:使用反射技術獲得File類中的私有構造函數 private File(String child, File parent)
* 並創建File類的對象獲得指定路徑的絕對路徑。
* 注:String child="柳巖.jpg",File parent=new File("D:\\test")
*/
Constructor constructor2 = clazz.getDeclaredConstructor(String.class,File.class);
//對於上述的構造函數是私有的,我們不能直接訪問,只能暴力訪問。
constructor2.setAccessible(true);
//創建File類的對象 使用獲得的構造函數創建對象 newInstance(Object... initargs)
File f2 = (File) constructor2.newInstance("柳巖.jpg",new File("D:\\test"));
System.out.println(f2.getAbsolutePath());//D:\test\柳巖.jpg
小結:
當要訪問Class對象中的私有的構造或成員時,需要使用getDeclaredXxxx()函數:
Xxxx表示:Constructor、Field、Method。
在訪問Class對象中的私有的構造函數或成員時,需要取消java語言的默認訪問權限檢查
setAccessible(boolean) true表示強制取消Java對私有成員的權限檢測。 false表示不會取消Java對私有成員的權限檢測。
2.3 反射獲取成員方法(Method)(掌握)
2.3.1 反射公開的非靜態的成員方法
說明:
1)在Class類中提供的getMethod方法上接收一個String name,name表示的是需要反射的那個方法的名字。
因爲在一個類中可以有多個不同名的方法。在反射的時候需要指定這個方法的名字,同時在一個類中還可能出現方法的重載,這時還需要指定具體反射的是哪個方法參數類型。
2)Method[] getMethods() 獲取所有公共的成員方法。包括父類的公共方法。
3)讓反射到的一個方法運行,需要使用Method類中的invoke方法 :
Object invoke(Object obj, Object… args)
invoke方法中的第一個參數 Object obj:表示的是當前需要調用這個方法的那個對象
invoke方法中的第二個參數Object… args:
表示的是真正需要運行的某個類中被反射的那個方法需要接收的真實參數
在調用Method類中的invoke方法的時候,其實底層是在運行被反射的那個方法,
既然是某個方法在運行,那麼方法運行完之後可能會有返回值。
舉例:需求:我們想通過反射技術獲得Person類中的setName()函數,並讓其執行。
/*
* 舉例:需求:我們想通過反射技術獲得Person類中的setName()函數,並讓其執行。
* Method getMethod(String name, Class<?>... parameterTypes)
* 返回一個 Method 對象,它反映此 Class 對象所表示的類或接口的指定公共成員方法。
* Method[] getMethods() 獲取所有的公共方法 包括父類的公共方法
*
* Object invoke(Object obj, Object... args) 表示讓反射到的一個方法運行
* obj:要執行哪個對象的方法
* args:方法需要的實際參數
*/
public class ReflectDemo {
public static void main(String[] args) throws Exception {
//獲取Class對象
Class clazz = Class.forName("cn.itcast.sh.reflect.demo.Person");
/*
* 反射成員方法:
* public void setName(String name)
* 類中的非靜態的成員方法,需要對象調用,我們反射到方法之後,最後肯定是要運行這個方法
* 這時肯定還是需要對象的
*
* Method getMethod(String name, Class<?>... parameterTypes)
* String name 反射的方法的名字
* Class<?>... parameterTypes 反射的方法接受的參數類型
*/
Method method = clazz.getMethod("setName", String.class);
//通過非反射的方式執行setName函數
/*
* Person p = new Person();
* p.setName("趙四");
* System.out.println(p.getName());//趙四
*/
//通過反射的方式執行setName函數
/*
* 讓反射到的一個方法運行,需要使用Method類中的invoke方法
*
* Object invoke(Object obj, Object... args)
*
* invoke方法中的第一個參數 Object obj:表示的是當前需要調用這個方法的那個對象
* invoke方法中的第二個參數Object... args:
* 表示的是真正需要運行的某個類中被反射的那個方法需要接收的真實參數
* 在調用Method類中的invoke方法的時候,其實底層是在運行被反射的那個方法,
* 既然是某個方法在運行,那麼方法運行完之後可能會有返回值
*/
//獲取此時clazz所表示Person類的一個新的對象
// Object obj = clazz.newInstance();
Person p = (Person) clazz.newInstance();
//執行setName函數 這句代碼就是在調用反射到Person類中的setName方法
Object obj2 = method.invoke(p, "趙四");
System.out.println(p);//cn.itcast.sh.reflect.demo.Person@7f21c5df
System.out.println(obj2);//null
System.out.println(p.getName());//趙四
System.out.println("=====================================");
//Method[] getMethods() 獲取所有的公共方法 包括父類的公共方法
Method[] methods = clazz.getMethods();
for (Method m : methods) {
System.out.println(m);
}
}
}
總結:
反射過程如下圖所示:
2.3.2反射類中的私有,靜態的,不需要參數的,且有返回值的方法
Method[] getDeclaredMethods() 獲取所有本類自己聲明過的成員方法。不包括父類中的方法。
Method getDeclaredMethod(String name,Class … parameterTypes)獲取某個方法。
案例:需求:反射Person類中的私有的 靜態的成員方法。private static void say()。
在Person類中添加如下函數:
private static void say()
{
System.out.println("say....");
}
說明:
1)如果在反射的時候,反射的方法不需要接受參數的時候,在反射時參數的類型可以書寫成null,由於第二個參數是可變參數,我們也可以什麼都不寫;
2)由於方法是私有的,這時需要取消Java的權限檢查,method.setAccessible(true);
如果不取消權限檢查會報異常:
3)調用invoke方法,讓被反射的方法運行, 由於這個方法say是靜態的,運行的時候是不需要對象的。這時在調用invoke方法的時候,對象書寫null,由於不需要參數,真實的參數也書寫成null。由於第二個參數是可變參數,我們也可以什麼都不寫。
具體代碼如下所示:
/*
* 案例:需求:反射Person類中的私有的 靜態的成員方法。private static void say()。
* Method[] getDeclaredMethods() 獲取所有本類自己聲明過的成員方法。不包括父類中的方法
* Method getDeclaredMethod(String name,Class ... parameterTypes)獲取某個方法。
*/
public class ReflectDemo1 {
public static void main(String[] args) throws Exception {
//調用自定義函數
demo();
}
//Method getDeclaredMethod(String name,Class ... parameterTypes)獲取某個方法。
//案例:需求:反射Person類中的私有的 靜態的成員方法。private static void say()。
public static void demo() throws Exception {
//獲取Class對象
Class clazz = Class.forName("cn.itcast.sh.reflect.demo.Person");
//Method[] getDeclaredMethods() 獲取所有本類自己聲明過的成員方法。不包括父類中的方法
//獲取所有的方法
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
System.out.println(m);
}
/*
* 反射方法say
* 反射的方法不需要接受參數的時候,在反射時參數的類型可以書寫成null
* 由於第二個參數是可變參數,我們也可以什麼都不寫。
*/
// Method method = clazz.getDeclaredMethod("say", null);
Method method = clazz.getDeclaredMethod("say");
/*
* 由於方法是私有的,這時需要取消Java的權限檢查
*/
method.setAccessible(true);
/*
* 調用invoke方法,讓被反射的方法運行
* 由於這個方法是靜態的,運行的時候是不需要對象的
* 這時在調用invoke方法的時候,對象書寫null,由於不需要參數,真實的參數也書寫成null
* 由於第二個參數是可變參數,我們也可以什麼都不寫
*/
// method.invoke(null, null);
Object obj = method.invoke(null);
System.out.println(obj);
}
}
代碼運行結果如下所示:
2.4 反射獲取成員變量(Field)屬於擴展內容,自己學習
需求:演示:反射獲取成員變量。
Field[] getFields() 獲取所有公共的成員變量
Field[] getDeclaredFields() 獲取所有成員變量 包括私有成員變量
Field getField(String name) 根據變量名獲取指定的公共成員變量
Field getDeclaredField(String name)根據變量名獲取指定的成員變量 包括私有成員變量
問題:拿到字段能幹嘛?
使用Field 類中的函數獲取或修改字段的值:
get(Object obj)獲取指定對象上當前字段的值。
set(Object obj,Object value) 將obj對象上此 Field 表示的字段設置爲指定的值
獲取字段類型:
Class getType() 獲取字段的數據類型的Class對象
需求:反射獲取Person類中的name、age、address屬性值並修改其值。
代碼演示如下所示:
/*
* 演示:反射獲取成員變量
* Field[] getFields() 獲取所有公共的成員變量
* Field[] getDeclaredFields() 獲取所有成員變量
* Field getField(String name) 根據變量名獲取指定的公共成員變量
* Field getDeclaredField(String name)根據變量名獲取指定的成員變量
* 問題:拿到字段能幹嘛?
* 使用Field 類中的函數獲取或修改字段的值:
* get(Object obj)獲取指定對象上當前字段的值。
* set(Object obj,Object value) 將obj對象上此 Field 表示的字段設置爲指定的值
* 獲取字段類型:
* Class getType() 獲取字段的數據類型的Class對象
*/
public class ReflectDemo3 {
public static void main(String[] args) throws Exception {
//創建Person類的對象
Person p = new Person("柳巖",19,"上海");
//根據對象p獲得Class類的對象
Class clazz = p.getClass();
//Field[] getFields() 獲取所有公共的成員變量
/*
* public int cn.itcast.sh.reflect.demo.Person.age
* public java.lang.String cn.itcast.sh.reflect.demo.Person.address
*/
Field[] fields = clazz.getFields();
for (Field field : fields) {
System.out.println(field);
}
System.out.println("------------------");
//Field[] getDeclaredFields() 獲取所有成員變量
/*
* private java.lang.String cn.itcast.sh.reflect.demo.Person.name
* public int cn.itcast.sh.reflect.demo.Person.age
* public java.lang.String cn.itcast.sh.reflect.demo.Person.address
*/
Field[] fields2 = clazz.getDeclaredFields();
for (Field field : fields2) {
System.out.println(field);
}
System.out.println("------------------");
//Field getField(String name) 根據變量名獲取指定的公共成員變量
//獲取age
Field f = clazz.getField("age");
System.out.println(f);//public int cn.itcast.sh.reflect.demo.Person.age
System.out.println("------------------");
//Field getDeclaredField(String name)根據變量名獲取指定的成員變量
Field f2 = clazz.getDeclaredField("name");
//private java.lang.String cn.itcast.sh.reflect.demo.Person.name
System.out.println(f2);
//get(Object obj)獲取指定對象上當前字段的值。
int age = (int) f.get(p);
System.out.println(age);//19
//set(Object obj,Object value) 將obj對象上此 Field 表示的字段設置爲指定的值
//需求,將年齡19改爲18
f.set(p, 18);
int age1 = (int) f.get(p);
System.out.println(age1);//18
System.out.println("------------------");
//暴力修改
f2.setAccessible(true);
f2.set(p, "鎖哥");
String name=(String) f2.get(p);
System.out.println(name);//鎖哥
//獲取字段類型
Class type = f.getType();
System.out.println(type);//int
}
}
2.5 反射的作用案例演示
-
作用
反射是框架的靈魂!框架的底層一定會用到反射技術。
-
需求:要把貓的睡覺方法 變成 狗的喫飯方法
- 效果:使用反射+Properties完成配置文件。把需要修改的靈活的內容寫在配置文件中,代碼不需要做任何的改動。
- 案例演示
public class Dog {
public void eat(){
System.out.println("狗愛喫肉");
}
public void sleep(){
System.out.println("狗睡覺流口水");
}
}
public class Cat {
public void eat(){
System.out.println("貓愛喫魚");
}
public void sleep(){
System.out.println("貓睡覺打呼嚕");
}
}
public class Demo {
public static void main(String[] args) throws Exception{
//不使用反射
//需求: 要把貓的睡覺方法 變成 狗的喫飯方法
//Dog d = new Dog();
//d.eat();
//使用反射
//properties
Properties pro = new Properties();
//load():可以把文件中的鍵值對讀取到集合中
FileReader fr = new FileReader("day21\\aaa.txt");
pro.load(fr);
//通過鍵獲取值
String cn = pro.getProperty("className");
String mn = pro.getProperty("methodName");
//獲取字節碼對象
Class c = Class.forName(cn);
//獲取空參構造
Constructor con = c.getConstructor();
//執行構造方法
Object o = con.newInstance();
//獲取方法
Method m = c.getMethod(mn);
//執行方法
m.invoke(o);
}
}
配置文件:
className=com.itheima_05.Cat
methodName=sleep
第3章 註解
3.1 註解概述
通過前面的演示,判斷一個子類方法是否是重寫父類的,或者判斷一個接口是否是函數式接口,我們都已經使用過類似@Override或者@FunctionalInterface.那麼這兩個都是什麼呢?這就是我們接下來要講的註解。
-
什麼是註解(Annotation)?
-
註解是JDK1.5的新特性,與類、接口是在同一個層次。
-
註解相當一種標記,是類的組成部分,可以給類攜帶一些額外的信息。
-
標記(註解)可以加在包,類,字段,方法,方法參數以及局部變量上。
-
註解是給編譯器或JVM看的,編譯器或JVM可以根據註解來完成對應的功能。
-
註解使用格式:@註解名
註解(Annotation)相當於一種標記,在程序中加入註解就等於爲程序打上某種標記,以後,javac編譯器、開發工具和其他程序可以通過反射來了解你的類及各種元素上有無何種 標記,看你的程序有什麼標記,就去幹相應的事,標記可以加在包、類,屬性、方法,方法的參數以及局部變量上。
-
3.2註解的作用
-
生成幫助信息:通過代碼裏標識註解,輔助生成幫助文檔對應的內容
/** 文檔註釋 * * @author wuyanzu 作者 * @version 1.0 版本 * */
-
編譯檢查:通過代碼裏標識註解,讓編譯器能夠實現基本的編譯檢查
@Override 檢查方法是一個重寫方法 @FunctionalInterface 檢查接口是一個函數式接口 @SuppressWarnings 表示抑制警告,不知道大家有沒有發現,我們所書寫的java代碼中經常出現一些黃色的標識。 這是一個警告,不是一個錯誤。但是我們可以通過@SupperssWarings註解來消除相應的警告。被修飾的類或方法如果存在編譯警告,將被編譯器忽略。 all,忽略所有
代碼演示如下:
public class SuppressWarningsDemo implements Serializable{ /* * SuppressWarnings 表示抑制警告 * all 表示忽略所有的警告 */ @SuppressWarnings("all") public void show() { //創建集合對象 List list=new ArrayList(); //向集合中添加數據 list.add("鎖哥"); String s=null; s.length(); } }
-
框架階段做配置
@Test junit的單元測試
3.3 自定義註解
3.3.1 定義—基本語法
上面我們學習了一些官方提供的註解。那麼,同樣我們開發者可以根據自己的需求來實現自定義註解。
註解和類,接口是屬於同一級別。所以創建註解我們需要使用@interface關鍵字。
自定義MyAnnotation1註解,步驟如下:
說明:
1)
A:定義類: class
B:定義接口:interface
C:定義註解使用關鍵字: @interface
可以自定義沒有屬性的註解,這時候我們已經可以使用該註解,可以在我們的方法或者變量上寫上@MyAnnotation1。註解的使用格式:@註解名。
只不過此時,當前這個註解只是定義了,沒有具體的意義。想要讓他有意義。我們需要通過後面的案例來給大家介紹。
新建一個Demo1類來使用上述自定義的註解。
上面我們定義的註解裏面沒有任何東西,相當於我們定義了一個class,裏面沒有任何東西。接下來我們學習一下定義帶有屬性的註解。
3)定義帶有屬性的註解。
屬性聲明的格式: 屬性類型 屬性名() default 屬性的默認值;
舉例:String value() default “黑旋風”;
int value2() default 19;
- 註解中的屬性類型可以是:基本類型、字符串String、Class、枚舉、註解,以及以上類型的一維數組。不能是其他的數據類型,比如自定義類型Person。
- 屬性名自定義,屬於標識符。但是同一註解中屬性名也不能相同。
- default 屬性的默認值 : 也是可以寫,可以不寫。
代碼如下:
/*
* 定義含有屬性的註解
*/
public @interface MyAnnotation1 {
//添加屬性
String value() default "呵呵";
}
3.3.2 使用自定義註解
剛剛在上面我們自定義了註解,那麼下面我們來使用這些註解。直接創建一個類,在類上寫上註解,註解使用的格式如下。
代碼如下:我們這裏定義的2個註解可以寫在類上,可以寫在方法上,可以寫在變量上。
使用格式:@註解類名( 屬性名= 值 , 屬性名 = 值 , …)
public class Demo2 {
@MyAnnotation1(x = "哈哈")
@MyAnnotation2(age = 10, names = {"黑旋風","柳巖"}, value = "鎖哥")
public void method()
{
System.out.println("method.....");
}
}
註解使用的注意事項:
1.註解可以沒有屬性,如果有屬性需要使用小括號括住。例如:@MyAnnotation1或@MyAnnotation2()
2.屬性格式:屬性名=屬性值,多個屬性使用逗號分隔。例如:@MyAnnotation2(age = 10, names = {“黑旋風”,“柳巖”}, value = “鎖哥”)
3.如果屬性類型爲數組,設置內容格式爲:{ 1,2,3 }。例如:arrs = {“itcast”,“itheima”}
4.如果屬性類型爲數組,值只有一個{} 可以省略的。例如:arrs = “itcast”
5.一個方法上,相同註解只能使用一次,不能重複使用。
6.如果屬性名爲value,且當前只有一個屬性,value可以省略。
7.如果使用多個屬性時,並且所有的屬性都沒有默認值。k的名稱爲value不能省略.
8.如果使用多個屬性時,並且所有的屬性都有默認值。k的名稱爲value可以省略.
代碼演示:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String name() default "abc";
String value() default "";
int age() default 0;
}
public class Test05 {
//@MyAnnotation(18)
//給屬性value賦值
@MyAnnotation("鎖哥")
public void show() {
}
}
注意:上面我們定義了自定義註解,也將註解使用到我們的類或者方法或者變量上面了。但是,可以發現,我們寫了這樣的註解,並不會產生任何的效果。所以,想要讓我們書寫的註解產生我們開發者所需要的效果,需要我們開發者使用java代碼,對我們的註解進行解析,讓這個註解產生我們所期望的功能。也就是說解析就是在解析註解中的屬性的值,解析屬性的值根據具體的業務功能來使用。所以接下來我們講解一下註解的解析。
3.3.3元註解介紹
元註解:用於修飾註解的註解。作用:限制定義的註解的特性。
例如:
我們要學習的元註解有兩種。
1、@Target 用於確定被修飾的註解使用的位置。
註解屬性:
1)ElementType.TYPE 修飾類、接口;
2)ElementType.CONSTRUCTOR 修飾構造方法;
3)ElementType.METHOD 修飾方法;
4)ElementType.FIELD 修飾字段;
對於註解@Override是不能修飾類的,只能修飾方法,而抑制警告註解@SuppressWarnings是可以修飾類、方法等,那麼具體哪個註解修飾方法或者類,是由什麼決定的呢?其實主要就是由元註解決定的。
先看註解@Override的底層代碼:
說明:對於@Override註解只能修飾方法。
再看註解@SuppressWarnings的底層代碼:
說明:對於@SuppressWarnings註解可以修飾所有的內容。
總結:所以對於元註解@Target表示修飾的註解作用範圍,能夠使用在哪裏。
案例:需求一:在自定義註解MyAnnotation2上添加一個元註解,要求自定義註解MyAnnotation2只能使用在方法上。
由於自定義註解要求只能使用在方法上,這裏使用在類上就會報異常。
案例:需求二:在自定義註解MyAnnotation2上添加一個元註解,要求自定義註解MyAnnotation2只能使用在方法和類上。
2、@Retention 用於確定被修飾的自定義註解生命週期。就是使用這個元註解修飾的註解可以存活到什麼時候。
在元註解@Retention中可以使用如下幾個屬性:
RetentionPolicy.SOURCE 被修飾的註解只能存在源碼中,即.java文件中。字節碼.class文件中沒有。用途:提供給編譯器使用。
RetentionPolicy.CLASS 被修飾的註解只能存在源碼和字節碼中,運行時內存中沒有。用途:JVM, java虛擬機使用。
RetentionPolicy.RUNTIME 被修飾的註解存在源碼、字節碼、內存(運行時)。用途:取代xml配置。(xml屬於配置文件的一種,我們後面會學習)注意:因爲我們要解析註解,而解析註解需要使用反射技術,要想使用反射技術,那麼要求註解必須存在內存中,即在元註解@Retention中需要設置RetentionPolicy.RUNTIME。
注意:上面三個屬性表示當使用在元註解中,元註解所修飾的註解能夠使用到什麼時候。
舉例:
上述案例中使用元註解@Retention修飾了註解MyAnnotation2並且設置屬性RetentionPolicy.RUNTIME,那麼也就是說對於Demo2類中的方法
method上面的註解MyAnnotation2會一直存在包括在內存中,如果把屬性換爲RetentionPolicy.SOURCE ,那麼method方法上的註解只會在.java文件中出現。
3.3.4 解析自定義註解
如果給類、方法等添加註解,如果需要獲得註解上設置的數據,那麼我們就必須對註解進行解析。
接下來我們就開始解析註解中的屬性值:
1)首先定義一個註解,並設置屬性值,記住,在開發中這個註解不需要我們自己書寫。
說明:自定義的註解要求可以使用在方法和類上。
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation2 {
//自定義屬性 定義一個String類型的屬性,沒有默認值
String value();
//定義int類型的屬性,沒有默認值
int age();
//定義String類型的數組,沒有默認值
String[] names();
}
2)在定義一個Demo2類,然後在這個類中定義方法method,在這個方法上使用註解MyAnnotation2 並給屬性賦值。記住,在開發中,這個類是我們自己寫的。
public class Demo2 {
@MyAnnotation2(age = 10, names = {"黑旋風","柳巖"}, value = "鎖哥")
public void method()
{
System.out.println("method.....");
}
}
3)在定義解析類,用來解析註解上的屬性值,注意,在開發中這個解析類是框架底層定義的,不需要我們自己書寫。
/*
* 解析註解的類
*/
public class MyAnnotationParser {
//當框架執行時,執行main方法內部的代碼
public static void main(String[] args) throws NoSuchMethodException, SecurityException, NoSuchFieldException {
//使用反射技術解析使用了註解的類或者方法
//1.獲得類的字節碼文件對象
Class clazz=Demo2.class;
//在Class類中存在getAnnotation方法,可以根據註解的字節碼獲取註解
// MyAnnotation2 ann = (MyAnnotation2) clazz.getAnnotation(MyAnnotation2.class);
// System.out.println(ann.age());
//2.獲得使用註解的方法
Method m = clazz.getDeclaredMethod("method");
// //3.獲得該方法上的註解對象,與字節碼相關的對象上都存在一個getAnnotation方法
MyAnnotation2 ann = m.getAnnotation(MyAnnotation2.class);
//4.獲得註解的屬性值names={"柳巖","鎖哥"}
System.out.println(ann.names());
String[] names = ann.names();
for (String name : names) {
System.out.println(name);
}
//age=18
int age = ann.age();
System.out.println(age);
//value="哈哈"
String value = ann.value();
System.out.println(value);
//5.獲得屬性值後根據業務需求做一些功能
}
}
說明:在Class類和Method類中都存在方法:T getAnnotation(Class annotationClass) 表示獲得當前對象上指定的註解。
3.4案例:自定義@Test
案例分析
步驟:
1.模擬Junit測試,首先需要編寫自定義註解@MyTest,並添加元註解,保證自定義註解只能修飾方法,且在運行時可以獲得。
2.其次編寫目標類(測試類)MyTestDemo,然後給目標方法(測試方法)使用@MyTest註解。
3.最後編寫解析類TestParser,使用main方法模擬Junit的右鍵運行。
案例實現
步驟1:編寫自定義註解類@MyTest ,需要使用2個元註解來註解一下。自定義註解代碼如下。
@Target(ElementType.METHOD)//只能使用在方法上
@Retention(RetentionPolicy.RUNTIME)//運行時可以獲得
public @interface MyTest {
}
步驟2:編寫目標類MyTestDemo
自己編寫目標類,然後定義3個方法,在其中2個方法上添加我們自定義的註解,代碼如下:
public class MyTestDemo {
//定義方法使用MyTest註解
@MyTest
public void test1()
{
System.out.println("test1...");
}
@MyTest
public void test2()
{
System.out.println("test2...");
}
public void test3()
{
System.out.println("test3...");
}
}
步驟3:編寫測試方法
思路:首先獲得我們MyTestDemo 類中的所有方法,然後循環遍歷每個方法,如果方法上有我們自定義的註解的話,我們就執行這個方法,如果沒有我們自定義的註解的話,我們就不執行。獲取類中的所有方法和執行方法需要用到反射機制中的內容。
補充:獲取方法Method類的對象後使用Method類的父類AccessibleObject類中的方法:boolean isAnnotationPresent(Class annotationClass)表示當前對象是否有註解。
/*
* 解析註解的類
*/
public class TestParser {
public static void main(String[] args) throws Exception {
//1.獲取測試類的字節碼文件對象
Class clazz=MyTestDemo.class;
//2.獲取MyTestDemo類中的所有方法
Method[] methods = clazz.getMethods();
//3.遍歷所有的方法
for (Method method : methods) {
//判斷每個方法上是否含有註解
boolean boo = method.isAnnotationPresent(MyTest.class);
if(boo)
{
//存在註解MyTest,執行該方法
method.invoke(clazz.newInstance());
}
}
}
}
輸出結果: