第17章_反射機制

长名06發表於2024-08-21

該篇筆記,是因為想重新學一下Spring Cloud 和Spring Cloud Alibaba框架,但是b站尚矽谷的最新課程,使用SpringBoot3作為,單體服務的框架,而SpringBoot3最低要求JDK17,所以必須要學一下JDK8-JDK17之間的新特性。本來只想看,宋紅康老師課程的第18章JDK8-17新特性,但是覺得反射的API有點忘記,遂順便看了第17章_反射機制,故有此筆記。

本章專題與脈絡

1.反射(Refection)的概念

1.1 反射的出現背景

Java 程式中,所有的物件都有兩種型別:編譯時型別執行時型別,而很多時候物件的編譯時型別和執行時型別不一致。 Object obj = new String("hello"); obj.getClass()

例如:某些變數或形參的宣告型別是 Object 型別,但是程式卻需要呼叫該物件執行時型別的方法,該方法不是 Object 中的方法,那麼如何解決呢?

解決這個問題,有兩種方案:

方案 1:在編譯和執行時都完全知道型別的具體資訊,在這種情況下,我們可以直接先使用 instanceof運算子進行判斷,再利用強制型別轉換符將其轉換成執行時型別的變數即可。

方案 2:編譯時根本無法預知該物件和類的真實資訊,程式只能依靠執行時資訊來發現該物件和類的真實資訊,這就必須使用反射。

1.2 反射概述

Reflection(反射)是被視為動態語言的關鍵,反射機制允許程式在執行期間藉助與Reflection API取得任何類的內部資訊,並能直接操作任意物件的內部屬性及方法。

載入完類之後,在堆記憶體的方法區中就產生了一個Class型別的物件(一個類只有一個Class物件),這個物件就包含了完整的類的結構資訊。我們可以透過這個物件看到類的結構。這個物件就像一面鏡子,透過這個鏡子看到類的結構,所以,形象的稱之為反射。

從記憶體載入上看反射:

1.3 Java反射機制研究及應用

Java反射機制提供的功能:

  • 在執行時判斷任意一個物件所屬的類;
  • 在執行時構造任意一個類的物件;
  • 在執行時判斷任意一個類所具有的成員變數和方法;
  • 在執行時獲取泛型資訊;
  • 在執行時呼叫任意一個物件的成員變數和方法;
  • 在執行時處理註解;
  • 生成動態代理。

1.4 反射相關的API

java.lang.Class:代表一個類、java.lang.reflect.Method:代表類的方法、java.lang.reflect.Field:代表類的成員變數、java.lang.reflect.Constructor:代表類的構造器 java.lang.reflect.Type,型別介面,Class類實現類該介面...

1.5 反射的缺點

優點:

  • 提高了Java程式的靈活性和擴充套件性,降低了耦合性,提高自適應能力。
  • 允許程式建立和控制任何類的物件,無需提前硬編碼目標類。

缺點:

  • 反射的效能較低。
    • 反射機制主要應用在對靈活性和擴充套件性要求很高的系統框架上。
  • 反射程式碼會模糊程式內部邏輯,可讀性較差。

2.理解Class類並獲取CLass例項

要想解析一個類,必須要先獲取到該類的Class物件。而解析一個類或用反射解決具體的問題就是使用相關API:

  • java.lang.Class
  • java.lang.reflect.*

可以說,Class物件是反射的根源。

2.1 理解Class

2.1.1 理論

在Object類中定義了一下的方法,此方法被所有子類繼承:

public final native Class<?> getClass();//沒有具體方法體,這是本地方法,底層是JNI機制,呼叫的寫好的C\C++程式碼

以上的方法返回值的型別是一個 Class 類,此類是 Java 反射的源頭,實際上所謂反射從程式的執行結果來看也很好理解,即:可以透過物件反射求出類的名稱。

物件照鏡子後可以得到的資訊:某個類的屬性、方法和構造器、某個類到底實現了哪些介面。對於每個類而言,JRE 都為其保留一個不變的 Class 型別的物件。一個 Class 物件包含了特定某個結構(Class/Interface/Enum/Annotation/Primitive Type/void/[])的有關資訊。

Primitive Type代表的基本資料類,如int,long等八大基本資料型別。注意基本資料型別和其包裝類的Class物件不是同一個。

  • Class本身也是一個類
  • Class物件只能由系統建立物件
  • 一個載入的類在 JVM 中只會有一個Class例項
  • 一個Class物件對應的是一個載入到 JVM 中的一個.class檔案
  • 每個類的例項都會記得自己是由哪個Class例項所生成
  • 透過Class可以完整地得到一個類中的所有被載入的結構
  • Class類是Reflection的根源,針對任何你想動態載入、執行的類,唯有先獲得相應的Class物件
2.1.2 記憶體結構上


說明:上圖中字串常量池在 JDK6 中儲存在方法區;JDK7及以後,儲存在堆空間。

2.2 獲取Class類的例項(四種方法)

/**
 * 獲取Class例項的幾種方式
 * 掌握前三種
 */
@Test
public void demo01() throws ClassNotFoundException {

    //前提:若已知具體的類,透過類的 class 屬性獲取,該方法最為安全可靠,程式效能最高
    //1.呼叫執行時類的靜態屬性:class
    Class clazz1 = User.class;
    System.out.println(clazz1);
	
    //前提:已知某個類的例項,呼叫該例項的 getClass()方法獲取 Class 物件
    //2.呼叫執行時類的物件的getClass()方法
    User user = new User();
    Class clazz2 = user.getClass();

    System.out.println(clazz1 == clazz2);//true

    //前提:已知一個類的全類名,且該類在類路徑下,可透過 Class 類的靜態方法forName()獲取,
    可能丟擲 ClassNotFoundException
    //3. 呼叫Class的靜態方法forName(String className)
    String className = "com.changming06.example02_class.User";//全類名
    Class class3 = Class.forName(className);//使用較多,更體現反射的動態性
    System.out.println(class3 == clazz2);//true
    System.out.println(class3 == clazz1);//true
	
    //前提:可以用系統類載入物件或自定義載入器物件載入指定路徑下的型別
    //4.使用類的載入器的方式
    Class clazz4 = ClassLoader.getSystemClassLoader().loadClass(className);
    System.out.println(clazz4 == clazz1);//true
}

User類

package com.changming06.example02_class;
/**
 * @author 長名06
 * @year 2024
 */
public class User {

    private String username;

    private String password;

    public User() {
    }

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }


}

2.3 那些型別有Class物件

簡言之,所有 Java 型別!

(1)class:外部類,成員(成員內部類,靜態內部類),區域性內部類,匿名內部類 (2)interface:介面 (3)[]:陣列 (4)enum:列舉 (5)annotation:註解@interface (6)primitive type:基本資料型別 (7)void

Class c1 = Object.class;
Class c2 = Comparable.class;
Class c3 = String[].class;
Class c4 = int[][].class;
Class c5 = ElementType.class;
Class c6 = Override.class;
Class c7 = int.class;
Class c8 = void.class;
Class c9 = Class.class;

int[] a = new int[10];
int[] b = new int[100];
Class c10 = a.getClass();
Class c11 = b.getClass();
// 只要元素型別與維度一樣,就是同一個 Class
System.out.println(c10 == c11);

2.4 Class類的常用方法

方法名 功能說明
static Class forName(String name) 返回指定類名 name 的 Class 物件
Object newInstance() 呼叫預設建構函式,返回該 Class 物件的一個例項
getName() 返回此 Class 物件所表示的實體(類、介面、陣列類、基本型別或 void)名稱
Class getSuperClass() 返回當前 Class 物件的父類的 Class 物件
Class [] getInterfaces() 獲取當前 Class 物件的介面
ClassLoader getClassLoader() 返回該類的類載入器
Class getSuperclass() 返回表示此 Class 所表示的實體的超類的Class
Constructor[] getConstructors() 返回一個包含某些 Constructor 物件的陣列
Field[] getDeclaredFields() 返回 Field 物件的一個陣列
Method getMethod(String name,Class … paramTypes) 返回一個 Method 物件,此物件的形參型別為 paramType

3.類的載入與ClassLoader的理解

3.1 類的生命週期

類在記憶體中完整的生命週期:載入-->使用-->解除安裝。其中載入過程又分為:裝載、連結、初始化三個階段。

3.2 類的載入過程

當程式主動使用某個類時,如果該類還未被載入到記憶體中,系統會透過載入、連結、初始化三個步驟來對該類進行初始化。如果沒有意外,JVM 將會連續完成這三個步驟,所以有時也把這三個步驟統稱為類載入。

類的載入又分為三個階段:

(1)裝載(Loading)

將類的.class檔案讀入記憶體,併為之建立一個java.lang.Class物件。此過程由類載入器完成

(2)連結(Linking)

①驗證 Verify:確保載入的類資訊符合 JVM 規範,例如:以 cafebabe 開頭,沒有安全方面的問題。

②準備 Prepare:正式為類變數(static)分配記憶體並設定類變數預設初始值的階段,這些記憶體都將在方法區中進行分配。

③解析 Resolve:虛擬機器常量池內的符號引用(常量名)替換為直接引用(地址)的過程。

關於,解析的理解,虛擬機器常量池內的符號引用(常量名)替換為直接引用(地址)的過程。在類中假如有一個public String name的屬性宣告,因為要將類載入到虛擬機器,也就是載入到記憶體中,在記憶體的物理結構中,儲存字串的格式是Ox0011(地址符) -> 代表具體資料的二進位制程式碼,一個地址中儲存一些資料。

public String name本質上是儲存在檔案中的一些字串,不過這些字串是寫在.java的檔案中,才有了特殊的意義。但其本質依舊是字串,要想在記憶體中生成一個類的class物件,也就是體現該類結構的物件。則必須出現這些字串,也就是說,可用申請一大塊記憶體,將一個類中出現的字串都儲存在一大塊記憶體中,但是這樣對於出現在多個類中的字串,就會在記憶體中出現資料冗餘,佔據多的記憶體,所以將類中的字串,拆分成一個一個的字串,進行單個儲存,這樣使用相同的字串時,只用字串的地址符就行了,提高字串的利用率,降低佔據的記憶體。只是個人淺薄認知,如果不對,還請大神教導。

(3)初始化(Initialization)

  • 執行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯期自動收集類中所有類變數的賦值動作和靜態程式碼塊中的語句合併產生的。(類構造器是構造類資訊的,不是構造該類物件的構造器)。
  • 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化。
  • 虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確加鎖和同步。

3.3 類載入器(ClassLoader)

3.3.1 類載入器的作用

將 class 檔案位元組碼內容載入到記憶體中,並將這些靜態資料轉換成方法區的執行時資料結構,然後在堆中生成一個代表這個類的 java.lang.Class 物件,作為方法區中類資料的訪問入口。

類快取:標準的 JavaSE 類載入器可以按要求查詢類,但一旦某個類被載入到類載入器中,它將維持載入(快取)一段時間。不過 JVM 垃圾回收機制可以回收這些 Class 物件。

3.3.2 類載入器的分類(JDK8)

JVM 支援兩種型別的類載入器,分別為引導類載入器(Bootstrap ClassLoader)和自定義類載入器(User-Defined ClassLoader)。

從概念上來講,自定義類載入器一般指的是程式中由開發人員自定義的一類,類載入器,但是 Java 虛擬機器規範卻沒有這麼定義,而是將所有派生於抽象類ClassLoader 的類載入器都劃分為自定義類載入器。無論類載入器的型別如何劃分,在程式中我們最常見的類載入器結構主要是如下情況:


1)啟動類載入器(引導類載入器,Bootstrap ClassLoader)**

  • 這個類載入使用 C/C++語言實現的,巢狀在 JVM 內部。獲取它的物件時往往返回null。
  • 它用來載入 Java 的核心庫(JAVA_HOME/jre/lib/rt.jar 或 sun.boot.class.path 路徑下的內容)。用於提供 JVM 自身需要的類。
  • 並不繼承自 java.lang.ClassLoader,沒有父載入器。
  • 出於安全考慮,Bootstrap 啟動類載入器只載入包名為 java、javax、sun 等開頭的類。
  • 載入擴充套件類和應用程式類載入器,並指定為他們的父類載入器。

(2)擴充套件類載入器(Extension ClassLoader)

  • Java 語言編寫,由 sun.misc.Launcher$ExtClassLoader 實現。

  • 繼承於 ClassLoader 類

  • 父類載入器為啟動類載入器

  • 從 java.ext.dirs 系統屬性所指定的目錄中載入類庫,或從 JDK 的安裝目錄的 jre/lib/ext子目錄下載入類庫。如果使用者建立的 JAR 放在此目錄下,也會自動由擴充套件類載入器載入。

    (3)應用程式類載入器(系統類載入器,AppClassLoader)

  • java 語言編寫,由 sun.misc.Launcher$AppClassLoader 實現

  • 繼承於 ClassLoader 類

  • 父類載入器為擴充套件類載入器

  • 它負責載入環境變數 classpath 或系統屬性 java.class.path 指定路徑下的類庫

  • 應用程式中的類載入器預設是系統類載入器。

  • 它是使用者自定義類載入器的預設父載入器

  • 透過 ClassLoader 的 getSystemClassLoader()方法可以獲取到該類載入器

(4)使用者自定義類載入器(瞭解)

  • 在 Java 的日常應用程式開發中,類的載入幾乎是由上述 3 種類載入器相互配合執行的。在必要時,我們還可以自定義類載入器,來定製類的載入方式。
  • 體現 Java 語言強大生命力和巨大魅力的關鍵因素之一便是,Java 開發者可以自定義類載入器來實現類庫的動態載入,載入源可以是本地的 JAR 包,也可以是網路上的遠端資源。
  • 同時,自定義載入器能夠實現應用隔離,例如 Tomcat,Spring 等中介軟體和元件框架都在內部實現了自定義的載入器,並透過自定義載入器隔離不同的元件模組。這種機制比 C/C++程式要好太多,想不修改 C/C++程式就能為其新增功能,幾乎是不可能的,僅僅一個相容性便能阻擋住所有美好的設想。
  • 自定義類載入器通常需要繼承於 ClassLoader。

3.3.3 檢視某個類的類載入器物件

@Test
public void demo1(){
    //獲取系統類載入器
    ClassLoader classLoader1 = ClassLoader.getSystemClassLoader();
    System.out.println(classLoader1);//sun.misc.Launcher$AppClassLoader@18b4aac2

    
    //檢視某個類是哪個類載入器載入的
	ClassLoader classloader = Class.forName("exer2.ClassloaderDemo").getClassLoader();
    
    //獲取擴充套件類載入器
    ClassLoader classLoader2 = classLoader1.getParent();//獲取類載入器的父載入器
    System.out.println(classLoader2);//sun.misc.Launcher$ExtClassLoader@28a418fc

    //獲取引導類載入器:失敗
    ClassLoader classLoader3 = classLoader2.getParent();
    System.out.println(classLoader3);//null
}

@Test
public void demo2(){
    //使用者自定義使用的是系統類載入器載入的。
    ClassLoader classLoader = User.class.getClassLoader();
    System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

    //對於Java的核心類使用的是引導類載入器載入
    ClassLoader classLoader1 = String.class.getClassLoader();
    System.out.println(classLoader1);//null
}

4.反射的基本應用

有了 Class 物件,能做什麼?

4.1 應用1:建立執行時類的物件

這是反射機制應用最多的地方。建立執行時類的物件有兩種方式:

方式 1:直接呼叫 Class 物件的 newInstance()方法

要 求: 1)類必須有一個無引數的構造器。2)類的構造器的訪問許可權需要足夠。

方式 2:透過獲取構造器物件來進行例項化

方式一的步驟:

1)獲取該型別的 Class 物件

2)呼叫 Class 物件的 newInstance()方法建立物件

方式二的步驟:

1)透過 Class 類的 getDeclaredConstructor(Class … parameterTypes)取得本類的指定形參型別的構造器

2)向構造器的形參中傳遞一個物件陣列進去,裡面包含了構造器中所需的各個引數。

3)透過 Constructor 例項化物件。

如果構造器的許可權修飾符修飾的範圍不可見,也可以呼叫 setAccessible(true)

/**
 * 這裡只是為了演示,簡化程式碼量,在實際開發中,不要在方法定義中,丟擲頂層的異常
 * @throws Exception
 */
@Test
public void demo1() throws Exception{
    //獲取到class物件,有多種方式,見上筆記
    Class clazz = Cat.class;

    Object obj = clazz.newInstance();
    System.out.println(obj);
}

@Test
public void demo2() throws Exception{
    Class clazz = Cat.class;
    //未使用泛型
    Constructor constructor = clazz.getDeclaredConstructor(String.class, boolean.class);
    constructor.setAccessible(true);
    //如果訪問的構造器無許可權,其未顯示呼叫該方法設定為可訪問會報java.lang.IllegalAccessException
    Object instance = constructor.newInstance("白色", true);
    System.out.println(instance);
}

4.2 應用2:獲取執行時類的完整結構

可以獲取:包、修飾符、型別名、父類(包括泛型父類)、父介面(包括泛型父介面)、成員(屬性、構造器、方法)、註解(類上的、方法上的、屬性上的)。

4.2.1 相關API

//1.實現的全部介面
public Class<?>[] getInterfaces() 
//確定此物件所表示的類或介面實現的介面。
    
//2.所繼承的父類
public Class<? Super T> getSuperclass()
//返回表示此 Class 所表示的實體(類、介面、基本型別)的父類的 Class。
    
//3.全部的構造器
public Constructor<T>[] getConstructors()
//返回此 Class 物件所表示的類的所有 public 構造方法。
    
public Constructor<T>[] getDeclaredConstructors()
//返回此 Class 物件表示的類宣告的所有構造方法。
//Constructor 類中:
    
//取得修飾符: 
public int getModifiers();
//取得方法名稱: 
public String getName();
//取得引數的型別:
public Class<?>[] getParameterTypes();

//4.全部的方法
public Method[] getDeclaredMethods()
//返回此 Class 物件所表示的類或介面的全部方法
public Method[] getMethods() 
//返回此 Class 物件所表示的類或介面的 public 的方法
    
//Method 類中:
public Class<?> getReturnType()
//取得全部的返回值
public Class<?>[] getParameterTypes()
//取得全部的引數
public int getModifiers()
//取得修飾符
public Class<?>[] getExceptionTypes()
//取得異常資訊
    
//5.全部的 Field
public Field[] getFields()
//返回此 Class 物件所表示的類或介面的 public 的 Field。
    
public Field[] getDeclaredFields()
//返回此 Class 物件所表示的類或介面的全部 Field。
    
//Field 方法中:
public int getModifiers()
//以整數形式返回此 Field 的修飾符
public Class<?> getType() 
//得到 Field 的屬性型別
public String getName() 
//返回 Field 的名稱。
    
//6. Annotation 相關
get Annotation(Class<T> annotationClass)
getDeclaredAnnotations()
    
//7.泛型相關
//獲取父類泛型型別:
Type getGenericSuperclass()
//泛型型別:ParameterizedType
//獲取實際的泛型型別引數陣列:
getActualTypeArguments()

//8.類所在的包
Package getPackage()

4.2.2 獲取屬性及其相關細節

/**
 * @author 長名06
 * 反射方式獲取類中屬性資訊
 * @year 2024
 */
public class FieldsTest {

    @Test
    public void demo1(){
        Class clazz = Cat.class;
        Field[] fields = clazz.getFields();
        for(Field field : fields){
            System.out.println(field);
        }
    }

    @Test
    public void demo2(){
        Class clazz = Cat.class;
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields){
            System.out.println(field);
        }
    }

    //許可權符 變數型別 變數名
    @Test
    public void demo3(){
        Class clazz = Cat.class;
        Field[] fields = clazz.getDeclaredFields();
        for(Field field : fields){
            //1.許可權修飾符
            /**
             * 0x 是十六進位制
             * PUBLIC       = 0x00000001; 1 1
             * PRIVATE      = 0x00000002; 2 10
             * PROTECTED    = 0x00000004; 4 100
             * STATIC       = 0x00000008; 8 1000
             * FINAL        = 0x00000010; 1610000
             * ... 在Modifier類中定義了很多類似
             * public static final int FINAL = 0x00000010;的屬性
             * 設計的思想是,就是用二進位制的某一位是 1,來代表一種修飾符,整個二進位制中只有一位是 1,其餘都是 0
             * 這樣,也就是getModifiers的計算得到的int的值是多個修飾符對應的值加一起(即是一串固定的二進位制數),
             * 有下面的好處
             *      Modifier#isPrivate
             *      public static boolean isPrivate(int mod) {
             *         return (mod & PRIVATE) != 0;
             *     }
             * 方便進行判斷,該屬性中是否包括某個修飾符
             * 因為修飾符,是和二進位制中某位是一一對應的,getModifiers得到的結果是加運算,自然是有某個修飾符,與其對應的位置設為1,
             * 判斷是否有該修飾符,只要把得到的值modifiers和對應的修飾符的值,進行邏輯與元素判斷是否為0即可。
             */
            int modifiers = field.getModifiers();
            System.out.print(Modifier.toString(modifiers) + "\t");
            //判斷是否具有public修飾符,其他類似,不在舉例
            System.out.print(Modifier.isPublic(modifiers) + "\t");

            //2.資料型別
            Class<?> type = field.getType();
            System.out.print(type.getName() + "\t");

            //3.變數名
            String fieldName = field.getName();//引用型別是全類名,基本型別就是對應基本型別的關鍵字
            System.out.print(fieldName);

            System.out.println();
        }
    }

}

4.2.3 獲取所有的方法及相關細節

/**
 * @author 長名06
 * @year 2024
 */
public class MethodTest {

    @Test
    public void demo1() throws Exception{

        Class<?> clazz = Class.forName("com.changming06.example_apply.data.Cat");

        Method[] methods = clazz.getMethods();

        for(Method m : methods){
            System.out.println(m);
        }
    }

    @Test
    public void demo2() throws Exception{
        Class<?> clazz = Class.forName("com.changming06.example_apply.data.Cat");
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method m : declaredMethods) {
            System.out.println(m);
        }
    }

    @Test
    public void demo3() throws Exception{
        Class<?> clazz = Class.forName("com.changming06.example_apply.data.Cat");
        Method[] declaredMethods = clazz.getDeclaredMethods();
        for (Method m : declaredMethods) {
            //1.獲取方法宣告的註解
            Annotation[] annotations = m.getAnnotations();
            for (Annotation annotation : annotations){
                System.out.println(annotation);
            }

            //2.許可權修飾符
            System.out.print(Modifier.toString(m.getModifiers()) + "\t");

            //3.返回值型別
            System.out.print(m.getReturnType().getName() + "\t");

            //4.方法名
            System.out.print(m.getName());
            System.out.print("(");
            //5.形參列表
            Class<?>[] parameterTypes = m.getParameterTypes();
            if(!(parameterTypes == null && parameterTypes.length == 0)){
                boolean isFirst = true;
                for(int i = 0; i < parameterTypes.length;i++){
                    Class c = parameterTypes[i];
                    if(i == parameterTypes.length - 1){
                        System.out.print(c.getName() + " arg_" + i);
                        break;
                    }
                    System.out.print("," + c.getName() + " arg_" + i + ",");
                }
            }
            System.out.print(")");

            //6.丟擲的異常
            Class<?>[] exceptionTypes = m.getExceptionTypes();
            if(exceptionTypes.length > 0){
                System.out.print("throws");
                for (int i = 0; i < exceptionTypes.length; i++) {
                    if (i == exceptionTypes.length - 1) {
                        System.out.print(exceptionTypes[i].getName());
                        break;
                    }
                    System.out.print(exceptionTypes[i].getName() + ",");
                }
            }
            System.out.println();
        }
    }

}

4.2.4 獲取其他結構(構造器,父類,介面,包,註解等)

/**
 * @author 長名06
 * @year 2024
 */
public class OtherTest {

    /**
     * 獲取當前類中的所有的構造器
     */
    @Test
    public void demo1() {
        Class clazz = Cat.class;
        Constructor[] constructors = clazz.getDeclaredConstructors();
        for (Constructor c : constructors) {
            System.out.println(c);
        }

    }

    /**
     * 獲取執行時類的父類
     */
    @Test
    public void demo2() {
        Class clazz = Cat.class;
        Class superclass = clazz.getSuperclass();
        System.out.println(superclass);
    }

    /**
     * 獲取執行時類的所在的包
     */
    @Test
    public void demo3() {
        Class clazz = Cat.class;
        Package aPackage = clazz.getPackage();//package是關鍵字
        System.out.println(aPackage);
    }

    /**
     * 獲取執行時類的註解
     */
    @Test
    public void demo4() {
        Class clazz = Cat.class;
        Annotation[] annotations = clazz.getAnnotations();
        for (Annotation anno : annotations) {
            System.out.println(anno);//類上並無註解
        }
    }

    /**
     * 獲取執行時類所實現的介面
     */
    @Test
    public void demo5() {
        Class clazz = Cat.class;

        Class[] interfaces = clazz.getInterfaces();//獲取類實現介面的class物件的陣列
        for (Class c : interfaces) {
            System.out.println(c);
        }
        System.out.println("--------");

        AnnotatedType[] annotatedInterfaces = clazz.getAnnotatedInterfaces();//獲取帶註解的介面的class物件的陣列
        for (AnnotatedType anInterfaces : annotatedInterfaces) {
            if (anInterfaces.isAnnotationPresent(MyAnnotation.class)) {
                MyAnnotation annotation = anInterfaces.getAnnotation(MyAnnotation.class);
                System.out.println(annotation.value());
            }
            System.out.println(anInterfaces);
        }
        System.out.println("--------");

        Type[] types = clazz.getGenericInterfaces();
        //獲取當前clazz物件對應的類實現的帶泛型的介面的class物件的陣列,但是不帶泛型的介面的class物件,也會在返回值中
        for (Type type : types) {
            //獲取介面的全名稱
            String typeName = type.getTypeName();
            //如果介面有泛型,可用將type轉成ParameterizedType,
            //獲取父類的泛型,與此類似。因為Type的本質是Class類的父介面,
            //其物件例項是一個個Class的物件,也就是一個個類或介面或其他有class物件的Java結構(void,基本型別等)的唯一的class物件
            //這樣的理解條件下,介面和類的class無區別,因為在繼承父類和實現父介面時,都可以指定泛型(在其定義了泛型的前提下)
            if (type instanceof ParameterizedType) {//只有是有泛型的class物件,才進行強轉
                ParameterizedType parameterizedType = (ParameterizedType) type;
                Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();//獲取其泛型列表陣列
                System.out.println(Arrays.asList(actualTypeArguments));
            }


            System.out.println(typeName);
            System.out.println(type);//獲取介面資訊
        }
    }

    @Test
    public void demo6() {
        Class clazz = Cat.class;
        //獲取帶泛型的父類,Class類實現了Type介面
        Type genericSuperclass = clazz.getGenericSuperclass();
        System.out.println(genericSuperclass);//如果有父類是有泛型的會出現泛型,沒有輸出父類

        if (genericSuperclass instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
            Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
            System.out.println(((Class)actualTypeArguments[0]).getName());
        }
    }
}

4.2.5 獲取泛型父類資訊

public class GenericTest {
    public static void main(String[] args) {
        //(1)還是先獲取 Class 物件
        Class clazz = Son.class;//四種形式任意一種都可以
        Type type = clazz.getGenericSuperclass();//獲取父類
        // Father<String,Integer>屬於 ParameterizedType
        ParameterizedType pt = (ParameterizedType) type;//父類的class物件,轉成ParameterizedType可以獲取泛型資訊
        //(3)獲取泛型父類的泛型實參列表
        Type[] typeArray = pt.getActualTypeArguments();
        for (Type type2 : typeArray) {
            System.out.println(type2);
        }
    }
}
//泛型形參:<T,U>
class Father<T,U>{
}
//泛型實參:<String,Integer>
class Son extends Father<String,Integer>{
}

4.2.6 獲取內部類或外部類資訊(瞭解)

public Class<?>[] getClasses():返回所有公共內部類和內部介面。包括從超類繼承的公共類和介面成員以及該類宣告的公共類和介面成員。

public Class<?>[] getDeclaredClasses():返回 Class 物件的一個陣列,這些物件反映宣告為此 Class 物件所表示的類的成員的所有類和介面。包括該類所宣告的公共、保護、預設(包)訪問及私有類和介面,但不包括繼承的類和介面。

public Class<?> getDeclaringClass():如果此 Class 物件所表示的類或介面是一個內部類或內部介面,則返回它的外部類或外部介面,否則返回 null。

Class<?> getEnclosingClass() :返回某個內部類的外部類。

4.2.7 小 結

1.在實際的操作中,取得類的資訊的操作程式碼,並不會經常開發。

2.一定要熟悉 java.lang.reflect 包的作用,反射機制。

4.3 應用3:呼叫執行類的指定結構

4.3.1 呼叫指定的屬性

在反射機制中,可以直接透過 Field 類操作類中的屬性,透過 Field 類提供的set()和 get()方法就可以完成設定和取得屬性內容的操作。

/**
①獲取該型別的 Class 物件
Class clazz = Class.forName("包.類名");
②獲取屬性物件
Field field = clazz.getDeclaredField("屬性名");
③如果屬性的許可權修飾符不是 public,那麼需要設定屬性可訪問
field.setAccessible(true);
④建立例項物件:如果操作的是非靜態屬性,需要建立例項物件
Object obj = clazz.newInstance();
有公共的無參構造Object obj = 構造器物件.newInstance(實參...);//透過特定構造器物件建立例項物件
⑤設定指定物件 obj 上此 Field 的屬性內容field.set(obj,"屬性值");
如果操作靜態變數,那麼例項物件可以省略,用 null 表示
⑥取得指定物件 obj 上此 Field 的屬性內容Object value = field.get(obj);
如果操作靜態變數,那麼例項物件可以省略,用 null 表示
*/

/**
 * 反射應用3-1 呼叫知道的屬性
 *  public String color = "黑色";
 */
@Test
public void demo1() throws Exception {
    Class clazz = Cat.class;

    Cat cat = (Cat)clazz.newInstance();

    //只能獲取到public修飾的
    Field colorField = clazz.getField("color");
    colorField.set(cat,"白色");
    System.out.println(colorField.get(cat));
}

@Test
public void demo2() throws Exception {
    Class clazz = Cat.class;

    Cat cat = (Cat)clazz.newInstance();

    //1.透過clazz.getDeclaredField(String fieldName)獲取類中宣告的屬性,和訪問修飾符是否能訪問無關
    Field wildField = clazz.getDeclaredField("wild");
    //2.setAccessible(true),確保能訪問該屬性
    wildField.setAccessible(true);
    //3.Field的例項,的get(Object object)方法獲取object物件的fieldName屬性
    //set(Object object,Object value)設定object物件的fieldName屬性值
    wildField.set(cat,false);
    System.out.println(wildField.get(cat));
}

@Test
public void demo3() throws Exception {
    Class clazz = Cat.class;

    //1.透過clazz.getDeclaredField(String fieldName)獲取類中宣告的屬性,和訪問修飾符是否能訪問無關
    Field wildField = clazz.getDeclaredField("price");
    //2.setAccessible(true),確保能訪問該屬性
    wildField.setAccessible(true);
    //3.Field的例項,的get(Object object)方法獲取object物件的fieldName屬性
    //set(Object object,Object value)設定object物件的fieldName屬性值
//        wildField.set(clazz,300.0);//對於類變數,傳入的是該類的class物件用來獲取和設定
//        System.out.println(wildField.get(clazz));
    //對於類變數,傳入的是null值用來獲取和設定也可以,因為類變數,在獲取該屬性時,是透過類.class獲取的
    //相當於,建立該屬性物件時,已經指明瞭該屬性物件的類。而物件屬性不行,是因為象屬性,是和類的例項有關的,
    //必須得屬於一個物件例項,談物件屬性才有意義。
    wildField.set(null,300.0);
    System.out.println(wildField.get(null));
}

關於 setAccessible 方法的使用:

  • Method 和 Field、Constructor 物件都有 setAccessible()方法。

  • setAccessible 啟動和禁用訪問安全檢查的開關。

  • 引數值為 true 則指示反射的物件在使用時應該取消 Java 語言訪問檢查。

    • 提高反射的效率。如果程式碼中必須用反射,而該句程式碼需要頻繁的被呼叫,那麼請設定為 true。
    • 使得原本無法訪問的私有成員也可以訪問
  • 引數值為 false 則指示反射的物件應該實施 Java 語言訪問檢查。

4.3.2 呼叫指定的方法

/*(1)獲取該型別的 Class 物件
Class clazz = Class.forName("包.類名");
(2)獲取方法物件
Method method = clazz.getDeclaredMethod("方法名",方法的形參型別列表);
(3)建立例項物件
Object obj = clazz.newInstance();
(4)呼叫方法
Object result = method.invoke(obj, 方法的實參值列表);
如果方法的許可權修飾符修飾的範圍不可見,也可以呼叫
setAccessible(true)
如果方法是靜態方法,例項物件也可以省略,用 null 代替
*/
/*********************** 呼叫指定方法 ***********************/

@Test
public void demo4() throws Exception {
    Class clazz = Cat.class;

    Cat cat = (Cat) clazz.newInstance();

    //注意int.class和Integer.class不一樣,其他基本型別和其包裝類同理
    //1.clazz.getDeclaredMethod(String methodName, Class... args)獲取指定的方法
    Method showPriceMethod = clazz.getDeclaredMethod("showPrice", int.class);

    //2.設定可訪問
    showPriceMethod.setAccessible(true);

    //3.透過Method例項呼叫invoke(Object object,Object... args),即執行Method對於的方法的執行
    //returnValue返回值,就是方法的返回值
    //如果方法的返回值為void,則返回值是null
    Object returnValue = showPriceMethod.invoke(cat, 20);

    System.out.println(returnValue);
}

@Test
public void demo5() throws Exception {
    Class clazz = Cat.class;


    //注意int.class和Integer.class不一樣,其他基本型別和其包裝類同理
    //1.clazz.getDeclaredMethod(String methodName, Class... args)獲取指定的方法
    Method showColorMethod = clazz.getDeclaredMethod("showColor");

    //2.設定可訪問
    showColorMethod.setAccessible(true);

    //3.透過Method例項呼叫invoke(Object object,Object... args),即執行Method對於的方法的執行
    //returnValue返回值,就是方法的返回值
    //如果方法的返回值為void,則返回值是null
    Object returnValue = showColorMethod.invoke(null);

    System.out.println(returnValue);
}

4.3.3 呼叫指定構造器

  /*********************** 呼叫指定構造器 ***********************/

@Test
public void demo6() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Class clazz = Cat.class;

    //1.getDeclaredConstructor(Class... args),根據傳入對應構造器形參的class物件,獲取指定的構造器
    Constructor constructor = clazz.getDeclaredConstructor(String.class, boolean.class);

    //2.設定可訪問
    constructor.setAccessible(true);

    //3.反射建立物件
    Cat object = (Cat) constructor.newInstance("白色", true);

    System.out.println(object);
}

//使用Constructor替換clazz.newInstance();
@Test
public void demo7() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Class clazz = Cat.class;

    Constructor constructor = clazz.getDeclaredConstructor();

    constructor.setAccessible(true);
    Object obj = constructor.newInstance();
    System.out.println(obj);

}

5. 應用 4:讀取註解資訊

一個完整的註解應該包含三個部分: (1)宣告 (2)使用 (3)讀取

5.1 宣告註解

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;

/**
 * @author 長名06
 * @year 2024
 */
@Target({TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {

    String value();

}
package com.changming06.example_other.annotation;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.FIELD;

/**
 * @author 長名06
 * @year 2024
 */
@Target({FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {

    String columnName();

    String columnType();

}
  • 自定義註解可以透過四個元註解@Retention,@Target,@Inherited,@Documented,分別說明它的宣告週期,使用位置,是否被繼承,是否被生成到 API 文件中。
  • Annotation 的成員在 Annotation 定義中以無引數有返回值的抽象方法的形式來宣告,我們又稱為配置引數。返回值型別只能是八種基本資料型別、String 型別、Class型別、enum 型別、Annotation 型別、以上所有型別的陣列。
  • 可以使用 default 關鍵字為抽象方法指定預設返回值。
  • 如果定義的註解含有抽象方法,那麼使用時必須指定返回值,除非它有預設值。格式是“方法名 = 返回值”,如果只有一個抽象方法需要賦值,且方法名為 value,可以省略“value=”,所以如果註解只有一個抽象方法成員,建議使用方法名value。

5.2 使用自定義註解

/**
 * @author 長名06
 * @year 2024
 */
@Table("customer")
public class Customer {

    @Column(columnName = "cust_name",columnType = "varchar(20)")
    private String name;

    @Column(columnName = "cust_age",columnType = "int")
    public int age;

    public Customer() {
    }

    public Customer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

5.3 讀取和處理自定義註解

自定義註解必須配上註解的資訊處理流程才有意義。

我們自己定義的註解,只能使用反射的程式碼讀取。所以自定義註解的宣告週期必須是 RetentionPolicy.RUNTIME。

/**
 * @author 長名06
 * @year 2024
 */
public class AnnotationTest {

    //獲取類宣告上的註解
    @Test
    public void demo1(){
        Class<?> clazz = Customer.class;

        //想要反射的獲取註解,則該註解必須被@Retention(RetentionPolicy.RUNTIME)元註解修飾
        //且必須是RetentionPolicy.RUNTIME型別,這樣該註解資訊才會儲存到記憶體中

        //獲取類宣告的註解
        Table declaredAnnotation = clazz.getDeclaredAnnotation(Table.class);
        String annotationValue = declaredAnnotation.value();

        System.out.println(declaredAnnotation + "\t" + annotationValue);
    }

    //獲取屬性宣告的註解
    @Test
    public void demo2() throws NoSuchFieldException {
        Class<?> clazz = Customer.class;
        //獲取屬性
        Field nameField = clazz.getDeclaredField("name");
        //獲取屬性宣告的註解
        Column column = nameField.getDeclaredAnnotation(Column.class);

        System.out.println(column.columnName());
        System.out.println(column.columnType());

    }

}

6.體會反射的動態性

/**
 * @author 長名06
 * @year 2024
 */
public class ReflectTest {


    /**
     * 反射動態性
     * @param className
     * @return
     */
    public Object getInstance(String className) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {

        Class<?> clazz = Class.forName(className);
        Constructor<?> constructor = clazz.getDeclaredConstructor();
        return constructor.newInstance();
    }

    @Test
    public void demo() throws ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {

        String className = "com.changming06.example_other.dynamic.Person";
        System.out.println(getInstance(className));

    }


}
public class Apple implements Fruit{
    @Override
    public void squeeze() {
        System.out.println("榨一杯蘋果汁");
    }
}

public class Banana implements Fruit{
    @Override
    public void squeeze() {
        System.out.println("榨一杯香蕉汁");
    }
}

public interface Fruit {

    void squeeze();

}

public class Juicer {

    public void run(Fruit f){
        f.squeeze();
    }

}

public class Orange implements Fruit{
    @Override
    public void squeeze() {
        System.out.println("榨一杯橘子汁");
    }
}

可以在,配置檔案【config.properties】儲存全類名com.changming06.example_other.exer.Apple,完整類的建立,然後建立Juicer類,完整run()方法執行。

只是為了記錄自己的學習歷程,且本人水平有限,不對之處,請指正。

相關文章