註解與反射

solutide發表於2024-08-04

註解與反射

1. 註解(Annotation)

可以被其他程式讀取

在哪裡使用:

可以附加在 package,class,method,field等上面,相當於給他們新增了額外的輔助資訊。

可以透過反射機制程式設計實現對這些後設資料的訪問。

1.1常見內建註解

@Override: 重寫的註解。

@Deprecated:廢棄的註解。已過時或者有更好的方式,不推薦使用。

@SuppressWarnings("all"):鎮壓警告。可以放在類名或者方法上

1.2元註解

元註解的用就是負責註解其他註解。Java定義了4個標準的 meta-annotation型別,它們被用來提供對其他annotation型別作說明。

@Target:用於描述註解的使用範圍(即被描述的註解可以用在什麼地方)

@Retention表示需要在什麼級別儲存該註釋資訊,用於描述註解的生命週期

SOURCE (原始碼時有效)< CLASS() < RUNTIME

Docunment:說明該註解被包含在javadoc

@Inherited:說明子類可以繼承父類中的該註解

@Target(value = ElementType.METHOD)//方法上有效
@Retention(value = RetentionPolicy.RUNTIME)//執行時有效
public @interface testAnnotation {

}

Target其中的value引數ElementType是一個列舉類

public enum ElementType {
   /** Class, interface (including annotation type), or enum declaration */
   TYPE,

   /** Field declaration (includes enum constants) */
   FIELD,

   /** Method declaration */
   METHOD,

   /** Formal parameter declaration */
   PARAMETER,

   /** Constructor declaration */
   CONSTRUCTOR,

   /** Local variable declaration */
   LOCAL_VARIABLE,

   /** Annotation type declaration */
   ANNOTATION_TYPE,

   /** Package declaration */
   PACKAGE,

   /**
    * Type parameter declaration
    *
    * @since 1.8
    */
   TYPE_PARAMETER,

   /**
    * Use of a type
    *
    * @since 1.8
    */
   TYPE_USE
}
1.3 自定義註解

使用@interface自定義註解時,自動繼承了java.lang.annotation.Annotation介面

@Target(value = {ElementType.TYPE,ElementType.METHOD})//方法上有效
@Retention(value = RetentionPolicy.RUNTIME)//執行時有效
public @interface testAnnotation1 {
    //註解的引數: 引數型別 + 引數名()
    String name() default "";
    int age() default 0;
    int id() default -1; //預設值為-1,代表不存在
}

對於註解存在預設值,則可以不寫引數的value value才能省略註解中括號的引數名

@testAnnotation1(name = "自定義註解")
public class annotationTest {
    @testAnnotation1(name="方法上自定義註解")
    public void test(){
        System.out.println("測試元註解");
    }
}

2. 反射(Reflection)

2.1 java反射機制概述

動態語言:執行時程式碼可以根據某些條件改變自身結構

靜態語言:執行時結構不變的語言

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

Class c = Class.forName("java.lang.String")

反射

  • 優點:可以動態建立物件和編譯,體現很大的靈活性
  • 缺點:對效能有影響。使用反射基本上是一種解釋操作,我們可以告訴JVM,我們希望做什麼並且它滿足我們的要求。這類操作總是慢於直接執行的操作。

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

public class ReflectionTest1 {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = Class.forName("BaseLearn.Reflection.User");

        //System.out.println(c1);

        Class c2 = Class.forName("BaseLearn.Reflection.User");
        Class c3 = Class.forName("BaseLearn.Reflection.User");
        Class c4 = Class.forName("BaseLearn.Reflection.User");

        //一個類在記憶體中只有一個Class物件
        //一個類被載入後,類的整個結構都會被封裝在Class物件中
        //hashCode值相同
        System.out.println(c1.hashCode());
        System.out.println(c2.hashCode());
        System.out.println(c3.hashCode());
        System.out.println(c4.hashCode());
    }
}
2.2 理解Class類並獲取Class例項

Class物件是Reflection的根源,針對任何想動態載入、執行的類,唯有先獲得相應的Class物件。

image-20240729141628761

//Class類的建立方式有哪些
public class ReflectionTest2 {
    public static void main(String[] args) throws ClassNotFoundException {
        Person person = new student();
        System.out.println("這個人是"+person.name);

        //方式一:透過物件獲得
        Class c1 = person.getClass();
        System.out.println(c1.hashCode());
        //方式2:forName獲得
        Class c2 = Class.forName("BaseLearn.Reflection.student");
        System.out.println(c2.hashCode());
        //方式3:透過類名.class獲得
        Class c3 = student.class;
        System.out.println(c3.hashCode());
        //方法4:基本內建型別的包裝類都有一個TYPE屬性
        Class c4 = Integer.TYPE;
        System.out.println(c4);
        //獲得父類型別
        Class c5 = c1.getSuperclass();
        System.out.println(c5);
    }
}

哪些型別可以有Class物件

  • class: 外部類,成員(成員內部類,靜態內部類),區域性內部類,匿名內部類
  • interface: 介面
  • []: 陣列
  • enum:列舉
  • annotation:註解
  • primitive type:基本資料型別
  • void
Class c1 = Objects.class;
Class c2 = Comparator.class;
Class c3 = String[].class;
Class c4 = Override.class;
Class c5 = Integer.class;
Class c6 = void.class;
Class c7 = Class.class;
//只要元素型別與維度一樣,就是通一個Class
int[] int1 = new int[10];
int[] int2 = new int[100];
int[][] ints = new int[10][10];

System.out.println(int1.getClass().hashCode());
System.out.println(int2.getClass().hashCode());
System.out.println(ints.getClass().hashCode());
//結果
//1554874502
//1554874502
//1846274136
2.3 類的載入與ClassLoader

image-20240729144215220

類的載入與ClassLoader的理解

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

  2. 連結:將java類中的二進位制程式碼合併到JVM的執行狀態中的過程

    1. 驗證:確保載入的類資訊符合JVM規範,沒有安全方面的問題
    2. 準備:正式為類變數(static)分配記憶體並設定類變數預設初始值的階段,這些記憶體都將在方法去中進行分配
    3. 解析:虛擬機器常量池內的符號引用(常量名)替換為直接引用(地址)的過程
  3. 初始化:

    1. 執行類構造器()方法的過程,類構造器()方法是由編譯期自動收集類中所有類變數的賦值動作和靜態程式碼塊中的語句合併產生的(類構造器是構造類資訊的,不是構造該類物件的構造器)
    2. 當初始化一個類的時候,如果發現其父類還沒有進行初始化,則需要先觸發其父類的初始化
    3. 虛擬機器會保證一個類的()方法在多執行緒環境中正確加鎖和同步
public class ReflectionTest4 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(A.m);
    }
}
/*
1. 載入到記憶體中,會產生一個類對應class物件
2. 連結 連結結束後,設定類變數預設初始值 m=0
3. 初始化:
	<clinit>() {
		System.out.println("A類靜態程式碼塊初始化");
       	      m=300;
       	      m=100;
	}
*/
class A {
    static {
        System.out.println("A類靜態程式碼塊初始化");
        m=300;
    }

    static int m = 100;

    public A() {
        System.out.println("A類的無參構造初始化");
    }
}

image-20240729151104051

什麼時候會發生類初始化

  1. 類的主動引用(一定會發生類的初始化)
    • 當虛擬機器啟動,先初始化main方法所在的類
    • new 一個類的物件
    • 呼叫類的靜態成員(除了final常量)和靜態方法
    • 使用java.lang.reflect包的方法對類進行反射呼叫
    • 當初始化一個類,如果其父類沒有被初始化,則會先初始化它的父類
  2. 類的被動引用(不會發生類的初始化)
  • 當訪問一個靜態域時,只有真正宣告這個域的類才會被初始化。如:當透過子類引用父類的靜態變數,不會導致子類初始化
  • 透過陣列定義類引用,不會觸發此類的初始化
  • 引用常量不會觸發此類的初始化(常量在連結階段就存入呼叫類的常量池了)
//測試什麼時候會初始化
public class ReflectionTest5 {
    static {
        System.out.println("主類被載入");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        //1. 主動引用
        //son son = new son();
        //反射也會產生主動引用
        //Class aClass = Class.forName("BaseLearn.Reflection.son");

        //System.out.println(son.b);

        //son[] arr = new son[10];
        System.out.println(son.M);
    }
}

class father {
    static int b = 2;
    static {
        
        System.out.println("父類被載入");
    }
}

class son extends father {
    static {
        System.out.println("子類被載入");
        m=300;
    }
    static  int m = 100;
    static final int M = 1;
}

類載入器的作用:

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

類快取:

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

2.4建立執行時類的物件
2.5 獲取執行時類的完整結構

image-20240729154843177

2.6呼叫執行時類的指定結構

有了Class物件能做什麼

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

  • 類必須有一個無參的構造器
  • 類的構造器的訪問許可權需要足夠

難道沒有無參構造器就不能建立物件了嗎?

只要在操作的時候明確呼叫類中的構造器,並且將引數傳遞進去之後,才可以例項化操作

步驟:

  1. 透過Class 類的getDeclaredConstructor()方法取得本類的指定形參型別的構造器
  2. 向構造器的形參中傳遞一個物件陣列進去,裡面包含了構造器中所需的各個引數
  3. 透過Constructor例項化物件newInstance

透過反射呼叫類中的方法,透過Method類完成

  1. 透過Class類的getMethod(String name,Class...parameterTypes)方法取得一個Method物件,並設定此方法操作時所需要的引數型別
  2. 之後使用Object invoke(Object obj,Object[] args)進行呼叫,並在方法中傳遞要設定的obj物件的引數資訊。

注意

  • Method和Field,Constructor物件都有setAccessible()方法,其作用是啟動和進位制安全檢查的開關。引數值為true ,則指示反射的物件在使用時取消Java語言訪問檢查
    • 提高反射的效率,如果程式碼中必須要用反射,而該句程式碼需要頻繁的呼叫,設定為true
    • 使得原本無法訪問的私有成員也可以訪問
Class c1 = Class.forName("BaseLearn.Reflection.User");
        //構造一個物件
        //User user = (User) c1.newInstance(); //本質時呼叫了無參構造器

        //System.out.println(user);

        //透過構造器建立物件
//        Constructor declaredConstructor = c1.getDeclaredConstructor(String.class, int.class);
//        User user = (User) declaredConstructor.newInstance("hhj", 20);
//        System.out.println(user);


        //透過反射呼叫普通方法
        User user1 = (User) c1.newInstance();
        System.out.println("111"+user1.getName());
        //透過反射獲取一個方法
        Method setName = c1.getDeclaredMethod("setName", String.class);
        setName.invoke(user1,"kkk");
        System.out.println("222"+user1.getName());
2.6.1獲取泛型資訊

image-20240729210548414

2.6.2獲取註解資訊

相關文章