1. 程式人生 > 實用技巧 >JavaSE第24篇:反射、註解、單元測試

JavaSE第24篇:反射、註解、單元測試

核心概述:在後面的JavaWeb篇幅中,我們將會學習到Web相關的框架,若想要更好的從底層理解框架,那麼我們不得不深入學習Java的反射機制,本篇我們將學習反射、註解、單元測試以及lombok外掛的使用。

目錄

第一章:類的載入器

1.1-類的載入器介紹(瞭解)

什麼是類載入器

類載入器(class loader)用來載入 Java 類到 Java 虛擬機器。

類的載入器時機

  1. 建立類的例項。
  2. 類的靜態變數,或者為靜態變數賦值。
  3. 類的靜態方法。
  4. 使用反射方式來強制建立某個類或介面對應的java.lang.Class物件。
  5. 初始化某個類的子類。
  6. 直接使用java.exe命令來執行某個主類。

以上6個情況,只要有1個出現,那麼類的載入器就會將這個類的class檔案載入到記憶體中,我們就可以使用這個類了。java.lang.ClassLoader:是類的載入器的父類。

載入器的種類

  1. 引導類載入器BootstrapClassLoader。
  2. 擴充套件類載入器ExtClassLoader
  3. 應用類載入器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類常用方法

  1. T newInstance(Object... initargs) ,根據指定引數建立物件。
  2. 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-反射案例(練習)

需求

本案例目的,體驗反射的靈活性。

需求:寫一個"框架",不能改變該類的任何程式碼的前提下,可以幫我們建立任意類的物件,並且執行其中任意方法。

實現方式和步驟

實現方式:配置檔案 + 反射

實現步驟:

  1. 將需要建立的物件的全類名和需要執行的方法定義在配置檔案中
  2. 在程式中載入讀取配置檔案
  3. 使用反射技術來載入類檔案進記憶體
  4. 建立物件
  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

使用步驟

步驟:

  1. 編寫測試類,簡單理解Junit可以用於取代java的main方法。
  2. 在測試類方法上添加註解 @Test。
  3. @Test修飾的方法要求:public void 方法名() {…} ,方法名自定義建議test開頭,沒有引數。
  4. 新增Junit庫到lib資料夾中,然後進行jar包關聯。

使用:點選方法左側綠色箭頭,執行當前方法(方法必須標記@Test)。執行結果紅色:代表失敗;執行結果綠色:代表成功。

哪個方法想使用單元測試,就在方法上,添加註解: @Test

注意:

  • 該方法的返回值型別,必須寫為void
  • 該方法必須沒有引數列表

執行:

  • 方法上右鍵執行,執行的是含有@Test註解的方法
  • 類上右鍵執行,執行的是類當中含有@Test註解的所有方法
  • 綠條: 正常執行
  • 紅條: 出現問題,異常了

常用註解

  • @Test,用於修飾需要執行的測試方法。
  • @Before,修飾的方法會在測試方法之前被自動執行。
  • @After,修飾的方法會在測試方法執行之後自動被執行。

第四章:註解

4.1-概述(瞭解)

什麼是註解

註解(Annotation),也叫元資料。一種程式碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、介面、列舉是在同一個層次。它可以宣告在包、類、欄位、方法、區域性變數、方法引數等的前面,用來對這些元素進行說明,註釋。

註解的作用

  • 編寫文件:通過程式碼裡標識的註解生成文件【例如,生成文件doc文件】
  • 程式碼分析:通過程式碼裡標識的註解對程式碼進行分析【例如,註解的反射】
  • 編譯檢查:通過程式碼裡標識的註解讓編譯器能夠實現基本的編譯檢查【例如,Override】

常見註解

  1. @author:用來標識作者名
  2. @version:用於標識物件的版本號,適用範圍:檔案、類、方法。
  3. @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-使用自定義註解(重要)

在程式中使用(解析)註解的步驟(獲取註解中定義的屬性值)

  1. 獲取註解定義的位置的物件 (Class,Method,Field)
  2. 獲取指定的註解 getAnnotation(Class)
  3. 呼叫註解中的抽象方法獲取配置的屬性值

使用格式

​ @註解名(屬性名=屬性值,屬性名=屬性值,屬性名=屬性值...)

示例

首先,定義一個註解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);

使用反射獲取註解的資料

需求說明

  1. 定義註解Book,要求如下:
    • 包含屬性:String value() 書名
    • 包含屬性:double price() 價格,預設值為 100
    • 包含屬性:String[] authors() 多位作者
    • 限制註解使用的位置:類和成員方法上
    • 指定註解的有效範圍:RUNTIME
  2. 定義BookStore類,在類和成員方法上使用Book註解
  3. 定義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(重要)

案例分析

  1. 模擬Junit測試的註釋@Test,首先需要編寫自定義註解@MyTest,並新增元註解,保證自定義註解只能修飾方法,且在執行時可以獲得。
  2. 然後編寫目標類(測試類),然後給目標方法(測試方法)使用 @MyTest註解,編寫三個方法,其中兩個加上@MyTest註解。
  3. 最後編寫呼叫類,使用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,無參構造方法
  • 註解只能寫在類上。