Java中的注解和反射

文章首發(fā)我的博客,歡迎訪問:https://blog.itzhouq.cn/annotation-reflection

最近又回顧了一下 Java 中的注解和反射知識點,注解在日常開發(fā)中使用很多,但是反射比較少。值得注意的是 Java 的各種框架底層源碼中大量使用了注解和反射,閱讀源碼,這些是基本功,面試中這部分內容也經(jīng)常問到。這里面概念不多,內容略微有些枯燥,但是通過一些簡單的例子,能讓我們明白一些基本概念和 API 的使用。所以,說到底,這篇博客只能算是一個簡單的筆記,希望對你有幫助。 以前也寫過枚舉類和注解的相關筆記,可以看看歷史文章 Java枚舉類和注解梳理

1、什么是注解

注解 Annotation 是從JDK1.5 開始引入的新技術。

注解的作用:不是程序本身,可以對程序作出解釋,能被其他程序讀取到。

注解使用的位置:package、class、method、field 等上面,相當于給他們添加了額外的輔助信息。我們可以通過反射機制實現(xiàn)對這些元數(shù)據(jù)的訪問。

2、元注解

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

  • Target:用于描述注解的使用范圍,注解可以用在什么地方。隨便點擊一個注解,查看源碼可以看到這個位置使用 ElementType枚舉類表示,主要可以放在類上,方法上,屬性上等,我這就不細說了。
  • Retention:表示在什么級別保存該注解的信息,用于描述注解的生命周期。SOURCE < CLASS <RUNTIME。同樣看源碼,使用 RetentionPolicy枚舉類表示,有 SOURCECLASS, RUNTIME?;疽娒?。
  • Document:說明該注解會被包含在 Javadoc中。
  • Inherited:說明子類可以繼承父類的該注解。

3、自定義注解

使用 @interface 自定義注解時,自動繼承了 java.lang.annotation.Annotation 接口。

分析:

  • @interface:用來聲明一個注解,格式:public @interface 注解名{定義內容};
  • 其中的每一個方法實際上就是一個配置參數(shù);
  • 方法的名稱就是參數(shù)的名稱;
  • 返回的類型就是參數(shù)的類型(返回值只能是基本類型、Class、String、enum);
  • 可以通過 default 來聲明參數(shù)的默認值;
  • 如果只有一個參數(shù)成員,一般參數(shù)名為 value;
  • 注解元素必須要有值,我們定義注解元素時,經(jīng)常使用空字符串,0作為默認值。
// 自定義注解
public class Test {
    @MyAnnotation(age = 18, name = "Hello")
    public void test() {}

}

@interface MyAnnotation {
    // 注解的參數(shù):參數(shù)類型 + 參數(shù)名();
    String name() default "";
    int age();
    int id() default -1;
    String[] schools() default {"清華大學", "北京大學"};
}

4、什么是反射

反射(Reflection):是 Java 被視為動態(tài)語言的關鍵,反射機制允許程序在執(zhí)行期借助于 Reflection API 取得任何類的內部信息,并能直接操作任意對象的內部屬性及方法。

加載完類后,在堆內存的方法區(qū)中就產(chǎn)生了一個 Class 類型的對象(一個類只有一個 Class 對象),這個對象包含了完整的類的結構信息。我們可以通過這個對象看到類的結構。這個對象就像一面鏡子,透過和鏡子看到類的結構。所以我們形象地稱之為反射。

反射

Java發(fā)射的優(yōu)缺點:

優(yōu)點:可以實現(xiàn)動態(tài)創(chuàng)建對象和編譯,體現(xiàn)很大的靈活性;

缺點:對性能有影響,使用反射基本上是一種解釋操作,這類操作總是慢于直接執(zhí)行相同的操作。

public class Test01 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 通過反射獲取類的Class對象
        Class c1 = Class.forName("Test01");
        Class c2 = Class.forName("Test01");
        System.out.println(c1); // class Test01

        // 一個類在內存中只有一個Class對象
        // 一個類被加載后,類的整個結構都會被封裝在Class對象中。
        System.out.println(c1.hashCode()); // 685325104
        System.out.println(c2.hashCode()); // 685325104
    }
}

class User{
    private String name;
    private int age;
    public User() {}
    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", 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;
    }
}

5、Class 類及其創(chuàng)建方式

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

public final Class getClass()

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

Class 類的特點:

  • Class本身也是一個類
  • Class對象只能由系統(tǒng)建立對象
  • 一個加載的類在 JVM 中只會有一個Class實例
  • 一個Class對象對應的是一個加載到JVM中的一個.class文件
  • 每個類的實例都會記得自己是由哪一個Class實例所生成的
  • 通過Class可以完整的得到一個類中所有被加載的結構
  • Class類是Reflection的根源針對任何你想要動態(tài)加載、運行的類,唯有先獲取相依的Class對象。
/**
 * 測試Class類的創(chuàng)建方式有哪些
 */
public class Test02 {
    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()); // 460141958

        // 方式二:forName獲取
        Class c2 = Class.forName("Student");
        System.out.println(c2.hashCode()); // 460141958

        // 方式三:通過類名.class獲取【最為安全可靠,性能最高】
        Class c3 = Student.class;
        System.out.println(c3.hashCode()); // 460141958

        // 方式四:基本內置類型的包裝類都有一個Type屬性
        Class c4 = Integer.TYPE;
        System.out.println(c4); // int
    }
}

class Person {
    String name;
    public Person() {
    }
    public Person(String name) {
        this.name = name;
    }

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

class Student extends Person{
    public Student() {
        this.name = "學生";
    }
}

class Teacher extends Person{
    public Teacher() {
        this.name = "老師";
    }
}

注意:Class 類創(chuàng)建方式很大概率在面試中會被問到。

6、類的加載過程和ClassLoader的理解

Java內存

當程序主動使用某個類時,如果該類還未被加載到內存中,則系統(tǒng)會通過如下三個步驟來對該類進行初始化:

類的加載過程
  • 加載:將class文件字節(jié)碼內容加載到內存中,并將這些靜態(tài)數(shù)據(jù)轉換成方法區(qū)的運行時數(shù)據(jù)結構,然后生成一個代表這個類的java.lang.Class對象;
  • 鏈接:將類的二進制代碼合并到JVM的運行狀態(tài)之中的過程。
    • 驗證:確保的加載的類信息符合JVM規(guī)范,沒有安全方面的問題;
    • 準備:正式為類變量(static)分配內存并設置類變量默認值的階段,這些內存都將在方法區(qū)中進行分配;
    • 解析:虛擬機常量池內的符號引用(常量名)替換為直接引用(地址)的過程;
  • 初始化:
    • 執(zhí)行類構造器<clinit>()方法的過程。類構造器<clinit>()方法是由編譯器自動收集類中所有類變量的賦值動作和靜態(tài)代碼塊中的語句合并產(chǎn)生的;
    • 當初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行初始化,則需要先觸發(fā)其父類的初始化;
    • 虛擬機會保證一個類的<clinit>()方法在多線程環(huán)境中被正確加鎖和同步。
public class Test03 {
    public static void main(String[] args) {
        A a = new A();
        System.out.println(a.m);
    }
}

class A {
    static {
        System.out.println("A類靜態(tài)代碼塊初始化");
        m = 300;
    }

    static int m = 100;

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

//    A類靜態(tài)代碼塊初始化
//    A類的無參構造初始化
//    100
    /**
     * 過程分析:
     *      1. 加載到內存,會產(chǎn)生一個類對象Class對象
     *      2. 鏈接, 鏈接結束后 m = 0
     *      3. 初始化
     *      <clinit>(){
     *          System.out.println("A類靜態(tài)代碼塊初始化");
     *          m = 300;
     *          m = 100;
     *      }
     */
}
類加載內存分析

7、分析類的初始化

什么時候會發(fā)生類的初始化?

  • 類的主動引用:一定會發(fā)生類的初始化
    • 當虛擬機啟動,先初始化 main 方法所在的類
    • new 一個類的對象
    • 調用類的靜態(tài)方法(除了 final 常量)和靜態(tài)方法
    • 使用 java.lang.reflection包的方法對類進行反射調用
    • 當初始化一個類,如果其父類沒有被初始化,則會先初始化其父類
  • 類的被動引用:不會發(fā)生類的初始化
    • 當訪問一個靜態(tài)域時,只有真正聲明這個域的類才會被初始化。如:當通過子類引用父類的靜態(tài)變量,不會導致子類被初始化
    • 通過數(shù)組定義類的引用,不會觸發(fā)類的初始化
    • 引用常量不會觸發(fā)此類的初始化(常量在鏈接階段就存入調用類的常量池中了)。
// 測試類什么時候會被初始化
public class Test04 {
    static {
        System.out.println("Main類被加載");
    }

    public static void main(String[] args) throws ClassNotFoundException {
        // 1. 主動引用
        // Son son = new Son();
//        Main類被加載
//        父類被加載
//        子類被加載

        // 2. 反射也會產(chǎn)生主動引用
        // Class.forName("Son");
//        Main類被加載
//        父類被加載
//        子類被加載

        // 3. 不會產(chǎn)生類的引用方法
        // System.out.println(Son.b);
//        Main類被加載
//        父類被加載
//        3

        Son[] array = new Son[5]; // Main類被加載
    }
}

class Father{
    static int b = 3;
    static {
        System.out.println("父類被加載");
    }
}

class Son extends Father{
    static {
        System.out.println("子類被加載");
        m = 300;
    }

    static int m = 100;
    static final int M = 1;
}

8、類加載器的作用

類加載器的作用:將class文件字節(jié)碼內容加載到內存中,并將這些靜態(tài)數(shù)據(jù)轉換成方法區(qū)的運行時數(shù)據(jù)結構,然后再堆中生成這個類的java.lang.Class對象,作為方法區(qū)中類數(shù)據(jù)的訪問入口。

類緩存:標準的 JavaSE 類加載器可以按要求查找類,但一旦某個類被加載到類加載器中,他將維持加載(緩存)一段時間。不過 JVM 垃圾回收機制可以回收這些Class對象。

類的加載

類加載器作用:用來把類(class)裝載進內存的。JVM 規(guī)范定義了如下類型的類的加載器。

類的加載器
public class Test05 {
    public static void main(String[] args) throws ClassNotFoundException {
        // 獲取系統(tǒng)類加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 獲取系統(tǒng)類加載器的父類---> 擴展類加載器
        ClassLoader systemClassLoaderParent = systemClassLoader.getParent();
        System.out.println(systemClassLoaderParent); // sun.misc.Launcher$ExtClassLoader@1b6d3586

        // 測試當前類是哪個類加載器加載的
        ClassLoader classLoader = Class.forName("Test05").getClassLoader();
        System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 測試 JDK 內置的類是哪個加載器加載的
        ClassLoader loader = Class.forName("java.lang.Object").getClassLoader();
        System.out.println(loader); // null 引導類加載器
    }

}

9、獲取運行時類的完整結構

通過反射獲取運行時累的完整結構

Filed、Method、Constructor、Superclass、Interface、Annotation。

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

// 獲得類的信息
public class Test06 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException, NoSuchMethodException {
        Class c1 = Class.forName("pojo.User");

        // 獲得類的名稱
        System.out.println(c1.getName()); // 包名+類名  pojo.User
        System.out.println(c1.getSimpleName()); // 類名   User

        // 獲得類的屬性
        Field[] fields = c1.getFields(); // 只能找到public屬性

        Field[] declaredFields = c1.getDeclaredFields(); // 找到全部屬性 private int pojo.User.id

        // 獲得指定屬性的值
        Field name = c1.getDeclaredField("name");

        Method[] methods = c1.getMethods(); // 獲得本類及其父類的全部 public 方法
        Method[] declaredMethods = c1.getDeclaredMethods(); // 獲得本類的所有方法

        // 獲得指定方法
        Method getName = c1.getMethod("getName", null);
        Method setName = c1.getMethod("setName", String.class);

        // 獲得指定構造器
        Constructor[] constructors = c1.getConstructors();
        Constructor[] declaredConstructors = c1.getDeclaredConstructors();

        Constructor declaredConstructor = c1.getDeclaredConstructor(int.class, String.class);
        System.out.println(declaredConstructor); // public pojo.User(int,java.lang.String)

    }
}

10、動態(tài)創(chuàng)建對象執(zhí)行方法

有了 Class 對象之后,能做什么?

創(chuàng)建類的對象:調用 Class 對象的 newInstance() 方法。

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

思考?難道沒有無參構造器就不能創(chuàng)建對象了嗎?

答:只要操作的時候明確的調用類中的構造器。并將參數(shù)傳遞進去之后,才可以實例化操作。

import pojo.User;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

// 通過反射動態(tài)的創(chuàng)建對象
public class Test07 {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        // 獲得 Class 對象
        Class c1 = Class.forName("pojo.User");

        // 構造一個對象
        User user = (User) c1.newInstance(); // 本質調用了類的無參構造器
        System.out.println(user); // User{id=0, name='null'}

        // 通過構造器創(chuàng)建對象
        Constructor constructor = c1.getDeclaredConstructor(int.class, String.class);
        User jack = (User) constructor.newInstance(1, "Jack");
        System.out.println(jack); // User{id=1, name='Jack'}

        // 通過反射調用普通方法
        User user2 = (User) c1.newInstance();
        // 通過反射調用一個方法
        Method setName = c1.getDeclaredMethod("setName", String.class);
        setName.invoke(user2, "小米");
        System.out.println(user2); // User{id=0, name='小米'}

        // 通過反射操作屬性
        User user3 = (User) c1.newInstance();
        Field name = c1.getDeclaredField("name");
        // 不能直接操作私有屬性,需要取消安全監(jiān)測
        name.setAccessible(true);
        name.set(user3, "小黑");
        System.out.println(user3); // User{id=0, name='小黑'}
    }
}

Method 和 Field、Constructor 對象都有 setAccessible() 方法。

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

參數(shù)值為 true 則指示反射的對象在使用時應該取消 Java 語言訪問檢查。

11、反射操作泛型(generics)

Java 采用泛型擦除的機制來引入泛型,Java 中的泛型僅僅是給編譯器 javac 使用的,確保數(shù)據(jù)的安全性和免去強制類型轉換問題,但是一旦編譯完成,所有和泛型有關的類型全部擦除。

為了通過反射操作這些類型, Java 新增了 ParameteriedType, GenericArrayType, TypeVariableWildcardType 幾種類型來代表不能被歸一到 Class 類中的類型但是又和原始類型齊名的類型。

  • ParameteriedType:表示一個參數(shù)化類型,比如Collection<String>
  • GenericArrayType:表示一個元素類型是參數(shù)化類型或者類型變量的數(shù)組類型
  • TypeVariable:是各種類型變量的公共父接口
  • WildcardType :代表一種通配符類型的表達式。
// 獲取泛型(generics)信息
public class Test08 {
    public void test01 (Map<String, User> map, List<User> list) {
        System.out.println("test01");
    }

    public Map<String, User> test02 () {
        System.out.println("test02");
        return null;
    }

    public static void main(String[] args) throws NoSuchMethodException {
        Method method = Test08.class.getMethod("test01", Map.class, List.class);
        Type[] genericParameterTypes = method.getGenericParameterTypes();
        for (Type genericParameterType : genericParameterTypes) {
            System.out.println(genericParameterType);
            // java.util.Map<java.lang.String, pojo.User>
            // java.util.List<pojo.User>
            if (genericParameterType instanceof ParameterizedType) {
                Type[] actualTypeArguments = ((ParameterizedType) genericParameterType).getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    System.out.println("##" + actualTypeArgument);
//                    java.util.Map<java.lang.String, pojo.User>
//                    ##class java.lang.String
//                    ##class pojo.User
//                    java.util.List<pojo.User>
//                    ##class pojo.User
                }
            }
        }

        System.out.println("=======================");
        // 獲取返回值泛型
        Method method1 = Test08.class.getMethod("test02", null);
        Type genericReturnType = method1.getGenericReturnType();
        if (genericReturnType instanceof ParameterizedType) {
            Type[] actualTypeArguments = ((ParameterizedType) genericReturnType).getActualTypeArguments();
            for (Type actualTypeArgument : actualTypeArguments) {
                System.out.println("##" + actualTypeArgument);
//                ##class java.lang.String
//                ##class pojo.User
            }
        }
    }
}

12、反射操作注解

練習:ORM

使用注解和反射完成類和表結構映射。類和表結構對應,屬性和字段對應、對象和記錄對應。

ORM映射
import java.lang.annotation.*;
import java.lang.reflect.Field;

// 練習反射操作注解
public class Test09 {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class c1 = Class.forName("Student");
        // 通過反射獲得注解

        Annotation[] annotations = c1.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation); // @TableMy(value=db_student)
        }

        // 獲得注解的 value 的值
        TableMy tableMy = (TableMy) c1.getAnnotation(TableMy.class);
        String value = tableMy.value();
        System.out.println(value); // db_student

        // 獲得類指定的注解
        Field f = c1.getDeclaredField("id");
        FiledMy annotation = f.getAnnotation(FiledMy.class);
        System.out.println(annotation.columnName()); // db_id
        System.out.println(annotation.type()); // int
        System.out.println(annotation.length()); // 10
    }

}

// 類名的注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface TableMy{
    String value();
}

// 屬性的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface FiledMy {
    String columnName();
    String type();
    int length();
}

@TableMy("db_student")
class Student {
    @FiledMy(columnName = "db_id", type = "int", length = 10)
    private int id;
    @FiledMy(columnName = "db_age", type = "int", length = 10)
    private String name;
    @FiledMy(columnName = "db_name", type = "varchar", length = 3)
    private int age;

    public Student() {
    }

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

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    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;
    }
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容