JavaSE第24篇:反射、註解、單元測試
核心概述:在後面的JavaWeb篇幅中,我們將會學習到Web相關的框架,若想要更好的從底層理解框架,那麼我們不得不深入學習Java的反射機制,本篇我們將學習反射、註解、單元測試以及lombok外掛的使用。
目錄
第一章:類的載入器
1.1-類的載入器介紹(瞭解)
什麼是類載入器
類載入器(class loader)用來載入 Java 類到 Java 虛擬機器。
類的載入器時機
- 建立類的例項。
- 類的靜態變數,或者為靜態變數賦值。
- 類的靜態方法。
- 使用反射方式來強制建立某個類或介面對應的java.lang.Class物件。
- 初始化某個類的子類。
- 直接使用java.exe命令來執行某個主類。
以上6個情況,只要有1個出現,那麼類的載入器就會將這個類的class檔案載入到記憶體中,我們就可以使用這個類了。java.lang.ClassLoader
:是類的載入器的父類。
載入器的種類
- 引導類載入器BootstrapClassLoader。
- 擴充套件類載入器ExtClassLoader
- 應用類載入器AppClassLoader
1.2-引導類載入器(瞭解)
引導類載入器BootstrapClassLoader:是C++語言編寫,負責載入JDK核心類庫,核心類庫位置\jdk\jre\lib\下的jar包。
由於引導類載入器器在JVM內部,開發人員是不能直接操作的。
ClassLoader loader = String.class.getClassLoader();
程式的輸出結果是null,C++編寫的載入器,根本就不是Java中的類。
1.3-擴充套件類載入器(瞭解)
擴充套件類載入器ExtClassLoader:Java語言編寫的類載入器,負責載入JDK擴充套件類庫,類庫位置\jdk\lib\ext\下的jar包。
ClassLoader loader = DNSNameService.class.getClassLoader();
System.out.println(loader);
程式的輸出結果是:sun.misc.Launcher$ExtClassLoader@45ee12a7,ExtClassLoader類繼承URLClassLoader,URLClassLoader繼承SecureClassLoader,SecureClassLoader繼承ClassLoader。
1.4-應用類載入器(瞭解)
應用類載入器AppClassLoader:Java語言編寫的類載入器,負責載入我們定義的類和第三方jar包中的類。
ClassLoader loader = Test.class.getClassLoader();
System.out.println(loader);
程式的輸出結果是:sun.misc.Launcher$AppClassLoader@18b4aac2,AppClassLoader繼承URLClassLoader,URLClassLoader繼承SecureClassLoader,SecureClassLoader繼承ClassLoader。
1.5-類載入器的雙親委派(瞭解)
ClassLoader類定義了方法 ClassLoader getParent()
:返回父類載入器。
//獲取自己定義類的載入器,結果為AppClassLoader
ClassLoader loader = Test.class.getClassLoader();
//獲取AppClassLoader的父類載入器,結果為ExtClassLoader
System.out.println(loader.getParent());
//獲取ExtClassLoader的父類載入器,結果為null
System.out.println(loader.getParent().getParent());
結論: AppClassLoader的父類載入器是ExtClassLoader,ExtClassLoader的父類載入器是Bootstrap。
注意:ExtClassLoader是AppClassLoader的父載入器,並不是父類,他們沒有繼承關係。
誰用誰載入:當A類中使用了B類,那麼負責載入A類的載入器要去載入B類。
雙親委派機制:當AppClassLoader收到一個載入類的請求時,會先讓他的父類載入器ExtClassLoader嘗試載入,ExtClassLoader也會讓他的父類載入器Bootstrap嘗試載入,如果Bootstrap能載入,就載入該類。如果Bootstrap不能載入,則ExtClassLoader會進行載入,如果也不能載入,AppClassLoader會進行載入。
1.6-Class物件的建立(瞭解)
當一個類的class檔案被類載入器載入到記憶體後,類的載入器會創建出此class檔案的物件。class檔案的物件是Class類的物件,是反射技術的基石。
第二章:反射
2.1-什麼是反射(瞭解)
Java反射機制指的是在Java程式執行狀態中,對於任何一個類,都可以獲得這個類的所有屬性和方法;對於給定的一個物件,都能夠呼叫它的任意一個屬性和方法。這種動態獲取類的內容以及動態呼叫物件的方法稱為反射機制。
Java的反射機制允許程式設計人員在對類未知的情況下,獲取類相關資訊的方式變得更加多樣靈活,呼叫類中相應方法,是Java增加其靈活性與動態性的一種機制。
2.2-獲取Class物件的方式(重要)
獲取方式
示例
Student類
public class Student {
private String name;
private int age;
public Student(){
}
public Student(String name,int age) {
this.name = name;
this.age = age;
}
public void study(){
System.out.println("學生在學習");
}
public void eat(String s,double d){
System.out.println("帶引數方法:"+s+"::"+d);
}
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
獲取Student類的class檔案物件:
public static void main(String[] args)throws Exception{
Student student = new Student();
Class c1 = student.getClass();
System.out.println(c1);
Class c2 = Student.class;
System.out.println(c2);
Class c3 = Class.forName("com.it.communication.Student");
System.out.println(c3);
}
2.3-反射獲取構造方法(重要)
獲取Constructor類的方式
方式1:Constructor[] getConstructors()
,獲取所有的public修飾的構造方法。
方式2:Constructor getConstructor(Class... parameterTypes)
,根據引數型別獲取構造方法物件,只能獲得public修飾的構造方法。如果不存在對應的構造方法,則會丟擲 java.lang.NoSuchMethodException 異常。引數是可變引數,呼叫此方法時,可以不寫引數,獲取的空參構造。比如:
引數 String name,int age
呼叫此方法: String.class,int.class
Constructor類常用方法
- T newInstance(Object... initargs) ,根據指定引數建立物件。
- T newInstance(),空參構造方法建立物件。
獲取無引數的構造方法
public static void main(String[] args)throws Exception{
Class cla = Class.forName("com.it.communication.Student");
//獲取無引數構造方法
Constructor constructor = cla.getConstructor();
//執行構造方法
Object object = constructor.newInstance();
System.out.println(object);
}
獲取有參構造方法
public static void main(String[] args)throws Throwable{
Class cla = Class.forName("com.it.communication.Student");
//獲取有引數構造方法
Constructor constructor = cla.getConstructor(String.class, int.class);
//執行構造方法,傳遞實際引數
Object object = constructor.newInstance("張三",20);
System.out.println(object);
}
反射獲取構造方法的簡單方式
Class類中定義了方法 T newInstance()
,可以直接執行獲取到的構造方法。
Class類中定義了方法 T newInstance()
,可以直接執行獲取到的構造方法。
要求:被反射的類中必須有public許可權的無引數構造方法。
public static void main(String[] args)throws Throwable{
Class cla = Class.forName("com.it.communication.Student");
Object object = cla.newInstance();
System.out.println(object);
}
2.4-反射獲取成員方法(重要)
獲取Method的方式
方式1: Method[] getMethods()
,獲取所有的public修飾的成員方法,包括父類中的方法。
方式2:Method getMethod("方法名", 方法的引數型別... 型別)
,根據方法名和引數型別獲得一個方法物件,只能是獲取public修飾的
Method類中常用方法
方法:Object invoke(Object obj, Object... args)
- 返回值Object,表示呼叫方法後,該方法的返回值
- 根據引數args呼叫物件obj的該成員方法
- 如果obj=null,則表示該方法是靜態方法
反射獲取無引數方法
public static void main(String[] args)throws Throwable{
Class cla = Class.forName("com.it.communication.Student");
Object object = cla.newInstance();
//獲取study方法
Method method = cla.getMethod("study");
//執行方法,傳遞物件
method.invoke(object);
}
反射獲取有引數方法
public static void main(String[] args)throws Throwable{
Class cla = Class.forName("com.it.communication.Student");
Object object = cla.newInstance();
//獲取有引數的方法eat
Method method = cla.getMethod("eat",String.class,double.class);
//呼叫eat方法,傳遞實際引數
method.invoke(object,"吃飯",9.9);
}
2.5-反射案例(練習)
需求
本案例目的,體驗反射的靈活性。
需求:寫一個"框架",不能改變該類的任何程式碼的前提下,可以幫我們建立任意類的物件,並且執行其中任意方法。
實現方式和步驟
實現方式:配置檔案 + 反射
實現步驟:
- 將需要建立的物件的全類名和需要執行的方法定義在配置檔案中
- 在程式中載入讀取配置檔案
- 使用反射技術來載入類檔案進記憶體
- 建立物件
- 執行方法
注意:需要將配置檔案放在src目錄下,放在src目錄下的任何檔案,都會被編譯到classes目錄下,這樣做的目的是為了讓配置檔案跟隨編譯後的class檔案一起,因為交付使用者使用的是class檔案,而不是原始碼。
如何讀取src目錄下的檔案:使用類的載入器ClassLoader類的方法 :InputStream getResourceAsStream(String name)
- 此方法返回輸入流,該流從類目錄下讀取檔案
- 引數傳遞檔名
配置檔案
properties檔案:pro.properties
className=com.it.domain.Student
methodName=sleep
學生類
Student類
package com.it.domain;
public class Student {
public void sleep(){
System.out.println("sleep...");
}
}
反射測試類
RefectTest類
public static void main(String[] args)throws Throwable{
//獲取RefectTest類的載入器
ClassLoader classLoader = RefectTest.class.getClassLoader();
//載入器獲取輸入流,讀取pro.properties檔案
InputStream inputStream = classLoader.getResourceAsStream("pro.properties");
Properties properties = new Properties();
//集合IO關聯
properties.load(inputStream);
//獲取集合中的鍵值對,類名
String className = properties.getProperty("className");
//獲取集合中的鍵值對,方法名
String methodName = properties.getProperty("methodName");
//反射獲取指定類的class檔案物件
Class cla = Class.forName(className);
Object object = cla.newInstance();
//獲取指定的方法
Method method = cla.getMethod(methodName);
//執行方法
method.invoke(object);
}
第三章:單元測試
3.1-測試分類(瞭解)
- 黑盒測試:不需要寫程式碼,給輸入值,看程式是否能夠輸出期望的值。
- 白盒測試:需要寫程式碼的。關注程式具體的執行流程。
3.2-Junit(瞭解)
概述
Junit是一個Java語言的單元測試框架,屬於白盒測試,簡單理解為可以用於取代java的main方法。Junit屬於第三方工具,需要匯入jar包後使用。
jar包下載
連結:https://pan.baidu.com/s/1XO4TZk5iVT0y2vNXOVyr9g
提取碼:cmcq
使用步驟
步驟:
- 編寫測試類,簡單理解Junit可以用於取代java的main方法。
- 在測試類方法上添加註解 @Test。
- @Test修飾的方法要求:public void 方法名() {…} ,方法名自定義建議test開頭,沒有引數。
- 新增Junit庫到lib資料夾中,然後進行jar包關聯。
使用:點選方法左側綠色箭頭,執行當前方法(方法必須標記@Test)。執行結果紅色:代表失敗;執行結果綠色:代表成功。
哪個方法想使用單元測試,就在方法上,添加註解: @Test
注意:
- 該方法的返回值型別,必須寫為void
- 該方法必須沒有引數列表
執行:
- 方法上右鍵執行,執行的是含有@Test註解的方法
- 類上右鍵執行,執行的是類當中含有@Test註解的所有方法
- 綠條: 正常執行
- 紅條: 出現問題,異常了
常用註解
- @Test,用於修飾需要執行的測試方法。
- @Before,修飾的方法會在測試方法之前被自動執行。
- @After,修飾的方法會在測試方法執行之後自動被執行。
第四章:註解
4.1-概述(瞭解)
什麼是註解
註解(Annotation),也叫元資料。一種程式碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、介面、列舉是在同一個層次。它可以宣告在包、類、欄位、方法、區域性變數、方法引數等的前面,用來對這些元素進行說明,註釋。
註解的作用
- 編寫文件:通過程式碼裡標識的註解生成文件【例如,生成文件doc文件】
- 程式碼分析:通過程式碼裡標識的註解對程式碼進行分析【例如,註解的反射】
- 編譯檢查:通過程式碼裡標識的註解讓編譯器能夠實現基本的編譯檢查【例如,Override】
常見註解
- @author:用來標識作者名
- @version:用於標識物件的版本號,適用範圍:檔案、類、方法。
- @Override :用來修飾方法宣告,告訴編譯器該方法是重寫父類中的方法,如果父類不存在該方法,則編譯失敗。
4.2-自定義註解(重要)
定義格式
元註解
public @interface 註解名稱{
屬性列表;
}
註解本質上就是一個介面,該介面預設繼承Annotation介面。
public @interface MyAnno extends java.lang.annotation.Annotation {}
任何一個註解,都預設的繼承Annotation介面。
註解的屬性
屬性的作用:可以讓使用者在使用註解時傳遞引數,讓註解的功能更加強大。
屬性的格式:
- 格式1:資料型別 屬性名();
- 格式2:資料型別 屬性名() default 預設值;
示例
public @interface Student {
String name(); // 姓名
int age() default 18; // 年齡
String gender() default "男"; // 性別
}
// 該註解就有了三個屬性:name,age,gender
屬性適用的資料型別
- 八種基本資料型別(int,float,boolean,byte,double,char,long,short)。
- String型別,Class型別,列舉型別,註解型別。
- 以上所有型別的一維陣列。
4.3-使用自定義註解(重要)
在程式中使用(解析)註解的步驟(獲取註解中定義的屬性值)
- 獲取註解定義的位置的物件 (Class,Method,Field)
- 獲取指定的註解
getAnnotation(Class)
- 呼叫註解中的抽象方法獲取配置的屬性值
使用格式
@註解名(屬性名=屬性值,屬性名=屬性值,屬性名=屬性值...)
示例
首先,定義一個註解Book
- 包含屬性:String value() 書名
- 包含屬性:double price() 價格,預設值為 100
- 包含屬性:String[] authors() 多位作者
程式碼實現
public @interface Book {
// 書名
String value();
// 價格
double price() default 100;
// 多位作者
String[] authors();
}
使用註解
/**
* @author itleilei
* @version 1.0
*/
public class BookShelf {
@Book(value = "西遊記",price = 998,authors = {"吳承恩","白求恩"})
public void showBook(){
}
}
注意事項
- 如果屬性有預設值,則使用註解的時候,這個屬性可以不用賦值。
- 如果屬性沒有預設值,那麼在使用註解時一定要給屬性賦值。
特殊屬性Value
當註解中只有一個屬性且名稱是value,在使用註解時給value屬性賦值可以直接給屬性值,無論value是單值元素還是陣列型別。
// 定義註解Book
public @interface Book {
// 書名
String value();
}
// 使用註解Book
public class BookShelf {
@Book("西遊記")
public void showBook(){
}
}
或
public class BookShelf {
@Book(value="西遊記")
public void showBook(){
}
}
如果註解中除了value屬性還有其他屬性,且至少有一個屬性沒有預設值,則在使用註解給屬性賦值時,value屬性名不能省略。
// 定義註解Book
public @interface Book {
// 書名
String value();
// 價格
double price() default 100;
// 多位作者
String[] authors();
}
// 使用Book註解:正確方式
@Book(value="紅樓夢",authors = "曹雪芹")
public class BookShelf {
// 使用Book註解:正確方式
@Book(value="西遊記",authors = {"吳承恩","白求恩"})
public void showBook(){
}
}
// 使用Book註解:錯誤方式
public class BookShelf {
@Book("西遊記",authors = {"吳承恩","白求恩"})
public void showBook(){
}
}
// 此時value屬性名不能省略了。
4.4-註解之元註解(重要)
概述
預設情況下,註解可以用在任何地方,比如類,成員方法,構造方法,成員變數等地方。如果要限制註解的使用位置怎麼辦?那就要學習一個新的知識點:元註解。
- @Target
- @Retention
元註解之@Target
作用:指明此註解用在哪個位置,如果不寫預設是任何地方都可以使用。
可選的引數值在列舉類ElemenetType中包括:
TYPE: 用在類,介面上
FIELD:用在成員變數上
METHOD: 用在方法上
PARAMETER:用在引數上
CONSTRUCTOR:用在構造方法上
LOCAL_VARIABLE:用在區域性變數上
元註解之@Retention
作用:定義該註解的生命週期(有效範圍)。
可選的引數值在列舉型別RetentionPolicy中包括
SOURCE:註解只存在於Java原始碼中,編譯生成的位元組碼檔案中就不存在了。
CLASS:註解存在於Java原始碼、編譯以後的位元組碼檔案中,執行的時候記憶體中沒有,預設值。
RUNTIME:註解存在於Java原始碼中、編譯以後的位元組碼檔案中、執行時記憶體中,程式可以通過反射獲取該註解。
4.5-註解解析(重要)
通過Java技術獲取註解資料的過程則稱為註解解析。
與註解解析相關的介面
- Anontation:所有註解型別的公共介面,類似所有類的父類是Object。
- AnnotatedElement:定義了與註解解析相關的方法,常用方法以下四個:
boolean isAnnotationPresent(Class annotationClass); 判斷當前物件是否有指定的註解,有則返回true,否則返回false。
T getAnnotation(Class<T> annotationClass); 獲得當前物件上指定的註解物件。
Annotation[] getAnnotations(); 獲得當前物件及其從父類上繼承的所有的註解物件。
Annotation[] getDeclaredAnnotations();獲得當前物件上所有的註解物件,不包括父類的。
獲取註解資料的原理
註解作用在那個成員上,就通過反射獲得該成員的物件來得到它的註解。
如註解作用在方法上,就通過方法(Method)物件得到它的註解。
// 得到方法物件
Method method = clazz.getDeclaredMethod("方法名");
// 根據註解名得到方法上的註解物件
Book book = method.getAnnotation(Book.class);
如註解作用在類上,就通過Class物件得到它的註解。
// 獲得Class物件
Class c = 類名.class;
// 根據註解的Class獲得使用在類上的註解物件
Book book = c.getAnnotation(Book.class);
使用反射獲取註解的資料
需求說明:
- 定義註解Book,要求如下:
- 包含屬性:String value() 書名
- 包含屬性:double price() 價格,預設值為 100
- 包含屬性:String[] authors() 多位作者
- 限制註解使用的位置:類和成員方法上
- 指定註解的有效範圍:RUNTIME
- 定義BookStore類,在類和成員方法上使用Book註解
- 定義TestAnnotation測試類獲取Book註解上的資料
程式碼實現:
註解Book
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Book {
// 書名
String value();
// 價格
double price() default 100;
// 作者
String[] authors();
}
BookStore類
@Book(value = "紅樓夢",authors = "曹雪芹",price = 998)
public class BookStore {
}
測試類TestAnnotation類
public class TestAnnotation {
public static void main(String[] args) throws Exception{
System.out.println("---------獲取類上註解的資料----------");
test();
}
/**
* 獲取BookStore類上使用的Book註解資料
*/
public static void test(){
// 獲得BookStore類對應的Class物件
Class c = BookStore.class;
// 判斷BookStore類是否使用了Book註解
if(c.isAnnotationPresent(Book.class)) {
// 根據註解Class物件獲取註解物件
Book book = (Book) c.getAnnotation(Book.class);
// 輸出book註解屬性值
System.out.println("書名:" + book.value());
System.out.println("價格:" + book.price());
System.out.println("作者:" + Arrays.toString(book.authors()));
}
}
4.6-模擬Junit(重要)
案例分析
- 模擬Junit測試的註釋@Test,首先需要編寫自定義註解@MyTest,並新增元註解,保證自定義註解只能修飾方法,且在執行時可以獲得。
- 然後編寫目標類(測試類),然後給目標方法(測試方法)使用 @MyTest註解,編寫三個方法,其中兩個加上@MyTest註解。
- 最後編寫呼叫類,使用main方法呼叫目標類,模擬Junit的執行,只要有@MyTest註釋的方法都會執行。
程式碼實現
註解MyTest
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {
}
MyTestDemo
public class MyTestDemo {
@MyTest
public void test01(){
System.out.println("test01");
}
public void test02(){
System.out.println("test02");
}
@MyTest
public void test03(){
System.out.println("test03");
}
}
測試類
public class TestMyTest {
public static void main(String[] args) throws Exception{
// 獲得MyTestDemo類Class物件
Class c = MyTestDemo.class;
// 獲得所有的成員方法物件
Method[] methods = c.getMethods();
// 建立MyTestDemo類物件
Object obj = c.newInstance();
// 遍歷陣列
for (Method m:methods) {
// 判斷方法m上是否使用註解MyTest
if(m.isAnnotationPresent(MyTest.class)){
// 執行方法m
m.invoke(obj);
}
}
}
}
第五章:lombok
5.1-概述
Lombok通過增加一些“處理程式”,可以讓java變得簡潔、快速。
Lombok能以註解形式來簡化java程式碼,提高開發效率。開發中經常需要寫的javabean,都需要花時間去新增相應的getter/setter,也許還要去寫構造器、equals等方法,而且需要維護。
Lombok能通過註解的方式,在編譯時自動為屬性生成構造器、getter/setter、equals、hashcode、toString方法。出現的神奇就是在原始碼中沒有getter和setter方法,但是在編譯生成的位元組碼檔案中有getter和setter方法。這樣就省去了手動重建這些程式碼的麻煩,使程式碼看起來更簡潔些。
5.2-使用
新增lombox的jar包:lombok-1.18.8.jar
連結:https://pan.baidu.com/s/1b5-WEiapXtr6ZkaW0c69fQ
提取碼:ij77
IDE中安裝lombox外掛
搜尋
下載安裝
重啟IDE,並設定註解配置
5.3-lombok常用註解
@Getter和@Setter
- 作用:生成成員變數的get和set方法。
- 寫在成員變數上,指對當前成員變數有效。
- 寫在類上,對所有成員變數有效。
- 注意:靜態成員變數無效。
@toString
- 作用:生成toString()方法。
- 註解只能寫在類上。
@NoArgsConstructor和@AllArgsConstructor
- @NoArgsConstructor:無引數構造方法。
- @AllArgsConstructor:滿引數構造方法。
- 註解只能寫在類上。
@EqualsAndHashCode
- 作用:生成hashCode()和equals()方法。
- 註解只能寫在類上。
@Data
- 作用:生成get/set,toString,hashCode,equals,無參構造方法
- 註解只能寫在類上。