天天看點

Java基礎——注解與反射1. 簡介2. 注解3. 反射

1. 簡介

最近在學習架構技術時頻繁地遇到注解與反射相關的原理,之前學習的javaSE中雖然學過,但是記憶模糊,是以打算重新複習一下這方面的知識。

2. 注解

2.1 概念

Java 注解(Annotation)又稱 Java 标注,是 JDK5.0 引入的一種注釋機制。Java 語言中的類、方法、變量、參數和包等都可以被标注。

注解與注釋有相似的,他們都是對其标注的代碼的一種說明。但是,注解更加複雜:

  • 有明确的編寫文法:注解的聲明需要符合java的命名規範,且自定義注解需要滿足Java的文法;
  • 有明确的使用位置:注解的使用位置是需要指明的;
  • 有明确的有效期:一個注解的生命周期包括:源代碼,class檔案,和運作時。
  • 具有強制性:注解所規定的内容使用者在使用時必須遵守,否則編譯無法通過。

java中是使用反射的機制來讀取注解的。

2.2 核心機制

1. 元注解 meta-annotation

java中提供了四個元注解,即注解的注解,我們在編寫自己的注解時,需要關注這四個注解,并按照需求對這四個注解進行指派。

  • @Document:說明該注解将寫入javadoc中;
  • @Retention:說明該注解可以被儲存到什麼時候(SOURCE<CLASS<RUNTIME);
  • @Target: 描述該注解的使用範圍(METHOD,CLASS,FIELD);
  • @Inherited:說明子類可以繼承父類中的該注解。

我們來看一下Target注解的内容:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] 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,

    /**
     * Module declaration.
     *
     * @since 9
     */
    MODULE
}
           

2. 自定義注解

java中使用@interface來建立自己的注解,此時會自動幫我們繼承

java.lang.annotation.Annotation

接口。格式為:

@元注解
@interface MyAnnotation {
}
           

我們可以在注解中定義參數,其格式為

參數類型 參數名();

且隻能定義String、基本資料類型,數組和enum。注意:注解中參數的指派是沒有順序的。如果該注解隻有一個值,那麼說明時可以使用

value=xxx

的方式指派。

@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
//    注解的參數,如果使用了注解并且沒有為參數設定初始值,那麼就會報錯
    String name();
//    如果該參數的值預設是空,那麼不需要初始化
    String addr() default "";
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
}
           

使用value作為注解的參數名時,當隻有一個參數,在指派時可忽略字元串。

@MyAnnotation3
public class AnnotationTest {
//    在使用時可以忽略
    @MyAnnotation2("zhangsan")
    @MyAnnotation3
    public static void main(String[] args) {

    }
}
@Target(value = ElementType.TYPE)
@interface MyAnnotation1 {
//    注解的參數,如果使用了注解并且沒有為參數設定初始值,那麼就會報錯
    String name();
//    如果該參數的值預設是空,那麼不需要初始化
    String addr() default "";
    int id() default 0; // 0表示預設為0
    int sal() default -1; // -1表示報不存在
    String[] client() default {"zhangsan", "lisi"};
}
@MyAnnotation1(name="yindarui")
@Target(value = ElementType.METHOD)
@interface MyAnnotation2 {
//    使用value作為參數名
    String value();
}

//Target注解的value是一個數組,是以可以指定多個作用域
@Target(value={ElementType.TYPE, ElementType.METHOD})
@interface MyAnnotation3 {

}
           

3. 反射

3.1 概念

反射機制使得java具備了動态性,即在java程式運作的時候,程式可以根據運作時的代碼,改變自身的一些結構。

反射機制允許java程式在運作時,借助一些API來擷取一個類的全部資訊,并直接對該類中的屬性、方法進行操作。

3.2 核心機制

我們知道,java的代碼在編寫完成,通過編譯器編譯之後會生成

.class

檔案,這稱為位元組碼檔案。一個位元組碼檔案會通過類加載器加載到Java虛拟機中,在被調用時進行執行個體化。

**反射機制想建立對象也必須擷取到該對象的位元組碼檔案。**在java中,class檔案也是一個類。通常通過

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

,或者

Object.getClass()

獲得。

類加載機制

首先我們先了解一下class檔案。

先通過反射的方式擷取到class檔案:如下的三個HashCode值是相同的,說明class檔案在JVM運作時隻會儲存一份。一個類在被加載到記憶體中,其所有的内容都會被封裝到Class對象。

Class person1 = Class.forName("com.yindarui.annotation.Person");
Person person = new Person();
Class person2 = person.getClass();
System.out.println(person1.hashCode());
System.out.println(person2.hashCode());
//結果是一樣的
1967205423
1967205423
           

那麼到底什麼是Class類呢?所有的其他類都由Class類建立,但是Class類本身也是一個類。其封裝了一個對象(編寫的代碼)裡面的全部内容。JVM可以通過Class對象建立别的對象。同樣的,對于任何一個對象,JVM都可以通過對象的全限定名找到其Class檔案,并擷取該Class檔案的全部内容。

3.3 使用Class類

Class類用方法:

類型 方法 功能
static forName(String name) 傳回指定類名的對象
public Object newInstance() 通過class對象調用預設的構造函數傳回一個對象
public String getName() 傳回該class對象的全限定名
public Class getSuperClass() 傳回該class對象的父類的class對象
public Class[] getInterfaces() 傳回該class對象實作的全部接口
public ClassLoader getClassLoader() 傳回該class對象的類加載器
public Constructor[] getConstructors() 傳回該class對象實作的全部接口
public Method getMethod(String name, Class<?>… parameterTypes)) 傳回該class對象指定的方法對象
public Method[] getMethods()) 傳回該class對象的全部方法對象
public Field getDeclaredFields() 傳回Field的一個數組

如果你熟悉java類包含的内容,那麼你一定會很好了解Class類中提供的方法。這幾乎包括了一個類中所有的資訊。(還可以擷取Annotation,這就是架構中大量使用的)

1. 擷取Class檔案

ArrayList<Class> classes = new ArrayList<>();
classes.add(Object.class); //根類
classes.add(Iterable.class); // 接口
classes.add(Override.class); // 注解
classes.add(ElementType.class); // 枚舉類
classes.add(String[].class); // 數組
classes.add(String[][].class); // 數組
classes.add(int.class); // 基本資料類型
classes.add(void.class); // 空類型
classes.add(Class.class); // Class本身也是一個
for (int i = 0; i < 9; i++) {
    System.out.println(classes.get(i).hashCode());
}
// 結果:對于數組而言,一維數組和二維數組即使類型相同,其對應的Class檔案也不同。
1426407511
589431969
1967205423
42121758
20671747
257895351
874088044
783286238
705927765
           

2. java 的記憶體模型

Java基礎——注解與反射1. 簡介2. 注解3. 反射

類的加載過程是:

  1. load:将class檔案讀入記憶體,并建立一個Java.lang.Class對象。class檔案是靜态的代碼,需要将這些代碼轉化成運作時的一些資料結構,比如為一些值設計資料結構。
  2. link:将類的二進制資料處理并放入JRE;

    1. 驗證:檢查代碼,確定沒有錯誤;

    2. 準備:為類變量配置設定記憶體空間并設定預設的初始值;

    3. 解析:虛拟機中的常量池的符号引用替換直接引用。

  3. initialize:JVM對類進行初始化。首先會執行

    <clint>()

    方法,即類構造器,完成對類的初始化。初始化類的時候,必須先初始化其父類。

我們來看一下這個過程:靜态代碼塊會在類被加載時就執行,我們在擷取一個類的Class對象時就會把這個Class檔案加載進來。

要明确:加載隻是把檔案讀入,并進行配置設定一定的記憶體空間,而初始化一個類則會消耗大量的資源,使用反射的方式加載類屬于被動加載,很多時候不會觸發多餘的類的初始化:圖檔來自b站狂神視訊

Java基礎——注解與反射1. 簡介2. 注解3. 反射

3. 類加載器

在JVM的記憶體模型中,類加載器就是負責找到這個類的class檔案并加載到JVM中。存儲class檔案的地方叫方法區,這個區中儲存這加載過的class檔案,并提供一定的緩存機制,以減少頻繁的從磁盤中加載類檔案。

java提供了幾種類加載器:圖檔來自b站狂神視訊

Java基礎——注解與反射1. 簡介2. 注解3. 反射

所謂的不同的類加載器,其實就是負責加載不同地方的class檔案。

ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader);
ClassLoader parent = systemClassLoader.getParent();
System.out.println(parent); // AppClassLoader
ClassLoader parent1 = parent.getParent(); // PlatformClassLoader
System.out.println(parent1); // null
System.out.println(Object.class.getClassLoader()); // null
           

雙親委派機制

JVM在加載類的時候是從啟動加載器向下找的,即如果我們自定義一個java.lang.String類型,是無法被加載進JVM的。因為JVM通過boostrap ClassLoader 擷取時就發現了這個類,加載後就停止尋找了。

這樣做保證了Java代碼的執行的正确性與安全性。

4. 通過反射建立對象

使用newInstance方法建立對象需要該類有無參構造器。

此外,我們還可以用别的方法建立:

  1. Class類的getDeclaredConstructor() 擷取構造器;
  2. 向構造器中傳遞參數的class檔案,包括該構造器的所有參數;
  3. 通過Constructor進行執行個體化。
package com.yindarui.annotation;

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

/**
 *
 */
public class Constructor {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class c1 = Class.forName("com.yindarui.annotation.Student");
        java.lang.reflect.Constructor stuCon = c1.getDeclaredConstructor(int.class, String.class);
        Object yindarui = stuCon.newInstance(1, "yindarui");
        System.out.println(yindarui.toString());
        Method setId = c1.getDeclaredMethod("setId", int.class);
        System.out.println("通過invoke來修改id參數");
        setId.invoke(yindarui,20);
        System.out.println(yindarui.toString());
        System.out.println("通過invoke來修改name參數, 此時setName方法是私有的");
        Method setName = c1.getDeclaredMethod("setName", String.class);
        setName.setAccessible(true);
        setName.invoke(yindarui,"helloworld");
        System.out.println(yindarui.toString());
    }
}

class Student {
    private int id;
    private String name;

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

    public void setId(int id) {
        this.id = id;
    }
    private void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

           

結果:

Java基礎——注解與反射1. 簡介2. 注解3. 反射

總之,通過反射來操作屬性和方法時,一定要傳遞你要操作的對象。注意:如果你通路的屬性、方法或者構造器是私有的,那麼在調用之前一定要使用setAccessible來擷取權限。

5. 反射擷取注解

注解開發的核心就是利用反射機制擷取一個class檔案中的注解内容。通常有幾種方式:

  • 通過class擷取全部注解;
  • 通過指定某個注解名稱擷取注解;
  • 通過某個屬性或者方法擷取标記在其上的注解;
package com.yindarui.annotation;
import java.lang.annotation.*;
import java.lang.reflect.AnnotatedType;
import java.lang.reflect.Field;


@MyAnnotation5("hello")
public class ReflectionForAnnotation {
    @MyAnnotation4("id = 10")
    public int id;
    public static void main(String[] args) throws ClassNotFoundException, NoSuchFieldException {
        Class aClass = Class.forName("com.yindarui.annotation.ReflectionForAnnotation");
        Field id = aClass.getDeclaredField("id");
//        擷取全部的注解
        Annotation[] annotation2 = aClass.getAnnotations();
//        擷取指定的注解
        MyAnnotation5 annotation1 = (MyAnnotation5) aClass.getAnnotation(MyAnnotation5.class);
        System.out.println(annotation1);
        System.out.println(annotation1.value());
//       通過屬性名擷取标記在屬性上的注解,方法也一樣
        MyAnnotation4 annotation = id.getAnnotation(com.yindarui.annotation.MyAnnotation4.class);
//       擷取在id屬性上的全部注解
        Annotation[] annotations = id.getAnnotations();
        System.out.println(annotation.value());
    }
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface MyAnnotation4{
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface MyAnnotation5{
    String value();
}
           

結果:

Java基礎——注解與反射1. 簡介2. 注解3. 反射