小白都能學會的Java註解與反射機制

前言

Java註解和反射是很基礎的Java知識了,爲什麼還要講它呢?由於我在面試應聘者的過程當中,發現很多面試者不多使用過註解和反射,甚至有人只能說出@Override這一個註解。我建議你們仍是儘可能能在開發中使用註解和反射,有時候使用它們能讓你事半功倍,簡化代碼提升編碼的效率。不少優秀的框架都基本使用了註解和反射,在Spring AOP中,就把註解和反射用得淋漓盡致。java

什麼是註解

Java註解(Annotation)亦叫Java標註,是JDK5.0開始引入的一種註釋機制。 註解能夠用在類、接口,方法、變量、參數以及包等之上。註解能夠設置存在於不一樣的生命週期中,例如SOURCE(源碼中),CLASS(Class文件中,默認是此保留級別),RUNTIME(運行期中)。程序員

註解以@註解名的形式存在於代碼中,Java中內置了一些註解,例如@Override,固然咱們也能夠自定義註解。註解也能夠有參數,例如@MyAnnotation(value = "陳皮")。面試

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

那註解有什麼做用呢?其一是做爲一種輔助信息,能夠對程序作出一些解釋,例如@Override註解做用於方法上,表示此方法是重寫了父類的方法。其二,註解能夠被其餘程序讀取,例如編譯器,例如編譯器會對被@Override註解的方法檢測判斷方法名和參數等是否與父類相同,不然會編譯報錯;並且在運行期能夠經過反射機制訪問某些註解信息。數據庫

內置註解

Java中有10個內置註解,其中6個註解是做用在代碼上的,4個註解是負責註解其餘註解的(即元註解),元註解提供對其餘註解的類型說明。編程

註解 做用 做用範圍
@Override 檢查該方法是不是重寫方法。若是其繼承的父類或者實現的接口中並無該方法時,會報編譯錯誤。 做用在代碼上
@Deprecated 標記表示過期的,不推薦使用。能夠用於修飾方法,屬性,類。若是使用被此註解修飾的方法,屬性或類,會報編譯警告。 做用在代碼上
@SuppressWarnings 告訴編譯器忽略註解中聲明的警告。 做用在代碼上
@SafeVarargs Java 7開始支持,忽略任何使用參數爲泛型變量的方法或構造函數調用產生的警告。 做用在代碼上
@FunctionalInterface Java 8開始支持,標識一個匿名函數或函數式接口。 做用在代碼上
@Repeatable Java 8開始支持,標識某註解能夠在同一個聲明上使用屢次。 做用在代碼上
@Retention 標識這個註解的保存級別,是隻在代碼中,仍是編入class文件中,或者是在運行時能夠經過反射訪問。包含關係runtime>class>source。 做用在其餘註解上,即元註解
@Documented 標記這些註解是否包含在用戶文檔中javadoc。 做用在其餘註解上,即元註解
@Target 標記某個註解的使用範圍,例如做用方法上,類上,屬性上等等。若是註解未使用@Target,則註解能夠用於任何元素上。 做用在其餘註解上,即元註解
@Inherited 說明子類能夠繼承父類中的此註解,但這不是真的繼承,而是可讓子類Class對象使用getAnnotations()獲取父類被@Inherited修飾的註解 做用在其餘註解上,即元註解

自定義註解

使用@interface關鍵字自定義註解,其實底層就是定義了一個接口,並且自動繼承java.lang.annotation.Annotation接口。數組

咱們自定義一個註解以下:安全

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
    String value();
}

咱們使用命令javap反編譯咱們定義的MyAnnotation註解的class文件,結果顯示以下。雖然註解隱式繼承了Annotation接口,可是Java不容許咱們顯示經過extends關鍵字繼承Annotation接口甚至其餘接口,不然編譯報錯。數據結構

D:\>javap MyAnnotation.class
Compiled from "MyAnnotation.java"
public interface com.nobody.MyAnnotation extends java.lang.annotation.Annotation {
  public abstract java.lang.String value();
}

註解的定義內容以下:框架

  • 格式爲public @interface 註解名 {定義內容}
  • 內部的每個方法實際是聲明瞭一個參數,方法的名稱就是參數的名稱。
  • 返回值類型就是參數的類型,並且返回值類型只能是基本類型(int,float,long,short,boolean,byte,double,char),Class,String,enum,Annotation以及上述類型的數組形式。
  • 若是定義了參數,可經過default關鍵字聲明參數的默認值,若不指定默認值,使用時就必定要顯示賦值,並且不容許使用null值,通常會使用空字符串或者0。
  • 若是隻有一個參數,通常參數名爲value,由於使用註解時,賦值能夠不顯示寫出參數名,直接寫參數值。
import java.lang.annotation.*;

/**
 * @Description 自定義註解
 * @Author Mr.nobody
 * @Date 2021/3/30
 * @Version 1.0
 */
@Target(ElementType.METHOD) // 此註解只能用在方法上。
@Retention(RetentionPolicy.RUNTIME) // 此註解保存在運行時期,能夠經過反射訪問。
@Inherited // 說明子類能夠繼承此類的此註解。
@Documented // 此註解包含在用戶文檔中。
public @interface CustomAnnotation {
    String value(); // 使用時須要顯示賦值
    int id() default 0; // 有默認值,使用時能夠不賦值
}
/**
 * @Description 測試註解
 * @Author Mr.nobody
 * @Date 2021/3/30
 * @Version 1.0
 */
public class TestAnnotation {

    // @CustomAnnotation(value = "test") 只能註解在方法上,這裏會報錯
    private String str = "Hello World!";

    @CustomAnnotation(value = "test")
    public static void main(String[] args) {
        System.out.println(str);
    }
}

Java8 註解

在這裏講解下Java8以後的幾個註解和新特性,其中一個註解是@FunctionalInterface,它做用在接口上,標識是一個函數式接口,即只有有一個抽象方法,可是能夠有默認方法。ide

@FunctionalInterface
public interface Callback<P,R> {

    public R call(P param);
}

還有一個註解是@Repeatable,它容許在同一個位置使用多個相同的註解,而在Java8以前是不容許的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(OperTypes.class)
public @interface OperType {
    String[] value();
}
// 能夠理解@OperTypes註解做爲接收同一個類型上重複@OperType註解的容器
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperTypes {
    OperType[] value();
}
@OperType("add")
@OperType("update")
public class MyClass {
    
}

注意,對於重複註解,不能再經過clz.getAnnotation(Class<A> annotationClass)方法來獲取重複註解,Java8以後,提供了新的方法來獲取重複註解,即clz.getAnnotationsByType(Class<A> annotationClass)方法。

package com.nobody;

import java.lang.annotation.Annotation;

/**
 * @Description
 * @Author Mr.nobody
 * @Date 2021/3/31
 * @Version 1.0
 */
@OperType("add")
@OperType("update")
public class MyClass {

    public static void main(String[] args) {
        Class<MyClass> clz = MyClass.class;
        Annotation[] annotations = clz.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println(annotation.toString());
        }

        OperType operType = clz.getAnnotation(OperType.class);
        System.out.println(operType);

        OperType[] operTypes = clz.getAnnotationsByType(OperType.class);
        for (OperType type : operTypes) {
            System.out.println(type.toString());
        }
    }

}

// 輸出結果爲
@com.nobody.OperTypes(value=[@com.nobody.OperType(value=[add]), @com.nobody.OperType(value=[update])])
null
@com.nobody.OperType(value=[add])
@com.nobody.OperType(value=[update])

在Java8中,ElementType枚舉新增了兩個枚舉成員,分別爲TYPE_PARAMETER和TYPE_USE,TYPE_PARAMETER標識註解能夠做用於類型參數,TYPE_USE標識註解能夠做用於標註任意類型(除了Class)。

Java反射機制

咱們先了解下什麼是靜態語言和動態語言。動態語言是指在運行時能夠改變其自身結構的語言。例如新的函數,對象,甚至代碼能夠被引進,已有的函數能夠被刪除或者結構上的一些變化。簡單說便是在運行時代碼能夠根據某些條件改變自身結構。動態語言主要有C#,Object-C,JavaScript,PHP,Python等。靜態語言是指運行時結構不可改變的語言,例如Java,C,C++等。

Java不是動態語言,可是它能夠稱爲準動態語言,由於Java能夠利用反射機制得到相似動態語言的特性,Java的動態性讓它在編程時更加靈活。

反射機制容許程序在執行期藉助於Reflection API取得任何類的內部信息,並能直接操做任意對象的內部屬性以及方法等。類在被加載完以後,會在堆內存的方法區中生成一個Class類型的對象,一個類只有一個Class對象,這個對象包含了類的結構信息。咱們能夠經過這個對象看到類的結構。

好比咱們能夠經過Class clz = Class.forName("java.lang.String");得到String類的Class對象。咱們知道每一個類都隱式繼承Object類,Object類有個getClass()方法也能獲取Class對象。

Java反射機制提供的功能

  1. 在運行時判斷任意一個對象所屬的類
  2. 在運行時構造任意一個類的對象
  3. 在運行時判斷任意一個類具備的成員變量和方法
  4. 在運行時獲取泛型信息
  5. 在運行時調用任意一個對象的成員變量和方法
  6. 在運行時獲取註解
  7. 生成動態代理
  8. ...

Java反射機制的優缺點

  • 優勢:實現動態建立對象和編譯,有更加的靈活性。
  • 缺點:對性能有影響。使用反射實際上是一種解釋操做,即告訴JVM咱們想要作什麼,而後它知足咱們的要求,因此老是慢於直接執行相同的操做。

Java反射相關的主要API

  • java.lang.Class:表明一個類
  • java.lang.reflect.Method:表明類的方法
  • java.lang.reflect.Field:表明類的成員變量
  • java.lang.reflect.Constructor:表明類的構造器

咱們知道在運行時經過反射能夠準確獲取到註解信息,其實以上類(Class,Method,Field,Constructor等)都直接或間接實現了AnnotatedElement接口,並實現了它定義的方法,AnnotatedElement接口的做用主要用於表示正在JVM中運行的程序中已使用註解的元素,經過該接口提供的方法能夠獲取到註解信息。

java.lang.Class 類

在Java反射中,最重要的是Class這個類了。Class自己也是一個類。當程序想要使用某個類時,若是此類還未被加載到內存中,首先會將類的class文件字節碼加載到內存中,並將這些靜態數據轉換爲方法區的運行時數據結構,而後生成一個Class類型的對象(Class對象只能由系統建立),一個類只有一個Class對象,這個對象包含了類的結構信息。咱們能夠經過這個對象看到類的結構。每一個類的實例都會記得本身是由哪一個Class實例所生成的。

經過Class對象能夠知道某個類的屬性,方法,構造器,註解,以及實現了哪些接口等信息。注意,只有class,interface,enum,annotation,primitive type,void,[] 等纔有Class對象。

package com.nobody;

import java.lang.annotation.ElementType;
import java.util.Map;

public class TestClass {

    public static void main(String[] args) {

        // 類
        Class<MyClass> myClassClass = MyClass.class;
        // 接口
        Class<Map> mapClass = Map.class;
        // 枚舉
        Class<ElementType> elementTypeClass = ElementType.class;
        // 註解
        Class<Override> overrideClass = Override.class;
        // 原生類型
        Class<Integer> integerClass = Integer.class;
        // 空類型
        Class<Void> voidClass = void.class;
        // 一維數組
        Class<String[]> aClass = String[].class;
        // 二維數組
        Class<String[][]> aClass1 = String[][].class;
        // Class類也有Class對象
        Class<Class> classClass = Class.class;

        System.out.println(myClassClass);
        System.out.println(mapClass);
        System.out.println(elementTypeClass);
        System.out.println(overrideClass);
        System.out.println(integerClass);
        System.out.println(voidClass);
        System.out.println(aClass);
        System.out.println(aClass1);
        System.out.println(classClass);
    }
}

// 輸出結果
class com.nobody.MyClass
interface java.util.Map
class java.lang.annotation.ElementType
interface java.lang.Override
class java.lang.Integer
void
class [Ljava.lang.String;
class [[Ljava.lang.String;
class java.lang.Class

獲取Class對象的方法

  1. 若是知道具體的類,可經過類的class屬性獲取,這種方法最安全可靠而且性能最高。Class clz = User.class;
  2. 經過類的實例的getClass()方法獲取。Class clz = user.getClass();
  3. 若是知道一個類的全限定類名,而且在類路徑下,可經過Class.forName()方法獲取,可是可能會拋出ClassNotFoundException。Class clz = Class.forName("com.nobody.User");
  4. 內置的基本數據類型能夠直接經過類名.Type獲取。Class<Integer> clz = Integer.TYPE;
  5. 經過類加載器ClassLoader獲取

Class類的經常使用方法

  • public static Class<?> forName(String className):建立一個指定全限定類名的Class對象
  • public T newInstance():調用Class對象所表明的類的無參構造方法,建立一個實例
  • public String getName():返回Class對象所表明的類的全限定名稱。
  • public String getSimpleName():返回Class對象所表明的類的簡單名稱。
  • public native Class<? super T> getSuperclass():返回Class對象所表明的類的父類的Class對象,這是一個本地方法
  • public Class<?>[] getInterfaces():返回Class對象的接口
  • public Field[] getFields():返回Class對象所表明的實體的public屬性Field對象數組
  • public Field[] getDeclaredFields():返回Class對象所表明的實體的全部屬性Field對象數組
  • public Field getDeclaredField(String name):獲取指定屬性名的Field對象
  • public Method[] getDeclaredMethods():返回Class對象所表明的實體的全部Method對象數組
  • public Method getDeclaredMethod(String name, Class<?>... parameterTypes):返回指定名稱和參數類型的Method對象
  • myClassClass.getDeclaredConstructors();:返回全部Constructor對象的數組
  • public ClassLoader getClassLoader():返回當前類的類加載器

在反射中常常會使用到Method的invoke方法,即public Object invoke(Object obj, Object... args),咱們簡單說明下:

  • 第一個Object對應原方法的返回值,若原方法沒有返回值,則返回null。
  • 第二個Object對象對應調用方法的實例,若原方法爲靜態方法,則參數obj可爲null。
  • 第二個Object對應若原方法形參列表,若參數爲空,則參數args爲null。
  • 若原方法聲明爲private修飾,則調用invoke方法前,須要顯示調用方法對象的method.setAccessible(true)方法,纔可訪問private方法。

反射操做泛型

泛型是JDK 1.5的一項新特性,它的本質是參數化類型(Parameterized Type)的應用,也就是說所操做的數據類型被指定爲一個參數,在用到的時候再指定具體的類型。這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口和泛型方法。

在Java中,採用泛型擦除的機制來引入泛型,泛型能編譯器使用javac時確保數據的安全性和免去強制類型轉換問題,泛型提供了編譯時類型安全檢測機制,該機制容許程序員在編譯時檢測到非法的類型。而且一旦編譯完成,全部和泛型有關的類型會被所有擦除。

Java新增了ParameterizedTypeGenericArrayTypeTypeVariableWildcardType等幾種類型,能讓咱們經過反射操做這些類型。

  • ParameterizedType:表示一種參數化類型,好比Collection<String>
  • GenericArrayType:表示種元素類型是參數化類型或者類型變量的數組類型
  • TypeVariable:是各類類型變量的公共父接口
  • WildcardType:表明種通配符類型表達式
package com.nobody;

import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;

public class TestReflectGenerics {

    public Map<String, Person> test(Map<String, Integer> map, Person person) {
        return null;
    }

    public static void main(String[] args) throws NoSuchMethodException {
        // 獲取test方法對象
        Method test = TestReflectGenerics.class.getDeclaredMethod("test", Map.class, Person.class);
        // 獲取方法test的參數類型
        Type[] genericParameterTypes = test.getGenericParameterTypes();
        for (Type genericParameterType : genericParameterTypes) {
            System.out.println("方法參數類型:" + genericParameterType);
            // 若是參數類型等於參數化類型
            if (genericParameterType instanceof ParameterizedType) {
                // 得到真實參數類型
                Type[] actualTypeArguments =
                        ((ParameterizedType) genericParameterType).getActualTypeArguments();
                for (Type actualTypeArgument : actualTypeArguments) {
                    System.out.println("    " + actualTypeArgument);
                }
            }
        }

        // 獲取方法test的返回值類型
        Type genericReturnType = test.getGenericReturnType();
        System.out.println("返回值類型:" + genericReturnType);
        // 若是參數類型等於參數化類型
        if (genericReturnType instanceof ParameterizedType) {
            // 得到真實參數類型
            Type[] actualTypeArguments =
                    ((ParameterizedType) genericReturnType).getActualTypeArguments();
            for (Type actualTypeArgument : actualTypeArguments) {
                System.out.println("    " + actualTypeArgument);
            }
        }

    }
}

class Person {}

// 輸出結果
方法參數類型:java.util.Map<java.lang.String, java.lang.Integer>
    class java.lang.String
    class java.lang.Integer
方法參數類型:class com.nobody.Person
返回值類型:java.util.Map<java.lang.String, com.nobody.Person>
    class java.lang.String
    class com.nobody.Person

反射操做註解

在Java運行時,經過反射獲取代碼中的註解是比較經常使用的手段了,獲取到了註解以後,就能知道註解的全部信息了,而後根據信息進行相應的操做。下面經過一個例子,獲取類和屬性的註解,解析映射爲數據庫中的表信息。

package com.nobody;

import java.lang.annotation.*;

public class AnalysisAnnotation {

    public static void main(String[] args) throws Exception {
        Class<?> aClass = Class.forName("com.nobody.Book");
        // 獲取類的指定註解,而且獲取註解的值
        Table annotation = aClass.getAnnotation(Table.class);
        String value = annotation.value();
        System.out.println("Book類映射的數據庫表名:" + value);

        java.lang.reflect.Field bookName = aClass.getDeclaredField("bookName");
        TableField annotation1 = bookName.getAnnotation(TableField.class);
        System.out.println("bookName屬性映射的數據庫字段屬性 - 列名:" + annotation1.colName() + ",類型:"
                + annotation1.type() + ",長度:" + annotation1.length());
        java.lang.reflect.Field price = aClass.getDeclaredField("price");
        TableField annotation2 = price.getAnnotation(TableField.class);
        System.out.println("price屬性映射的數據庫字段屬性 - 列名:" + annotation2.colName() + ",類型:"
                + annotation2.type() + ",長度:" + annotation2.length());
    }
}

// 做用於類的註解,用於解析表數據
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Table {
    // 表名
    String value();
}

// 做用於字段,用於解析表列
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface TableField {
    // 列名
    String colName();

    // 列類型
    String type();

    // 長度
    int length();
}

@Table("t_book")
class Book {
    @TableField(colName = "name", type = "varchar", length = 15)
    String bookName;
    @TableField(colName = "price", type = "int", length = 10)
    int price;
}

// 輸出結果
Book類映射的數據庫表名:t_book
bookName屬性映射的數據庫字段屬性 - 列名:name,類型:varchar,長度:15
price屬性映射的數據庫字段屬性 - 列名:price,類型:int,長度:10

性能分析

前面咱們說過,反射對性能有必定影響。由於反射是一種解釋操做,它老是慢於直接執行相同的操做。並且Method,Field,Constructor都有setAccessible()方法,它的做用是開啓或禁用訪問安全檢查。若是咱們程序代碼中用到了反射,並且此代碼被頻繁調用,爲了提升反射效率,則最好禁用訪問安全檢查,即設置爲true。

package com.nobody;

import java.lang.reflect.Method;

public class TestReflectSpeed {

    // 10億次
    private static int times = 1000000000;

    public static void main(String[] args) throws Exception {
        test01();
        test02();
        test03();
    }

    public static void test01() {
        Teacher t = new Teacher();
        long start = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            t.getName();
        }
        long end = System.currentTimeMillis();
        System.out.println("普通方式執行10億次消耗:" + (end - start) + "ms");
    }

    public static void test02() throws Exception {
        Teacher teacher = new Teacher();
        Class<?> aClass = Class.forName("com.nobody.Teacher");
        Method getName = aClass.getDeclaredMethod("getName");
        long start = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            getName.invoke(teacher);
        }
        long end = System.currentTimeMillis();
        System.out.println("反射方式執行10億次消耗:" + (end - start) + "ms");
    }

    public static void test03() throws Exception {
        Teacher teacher = new Teacher();
        Class<?> aClass = Class.forName("com.nobody.Teacher");
        Method getName = aClass.getDeclaredMethod("getName");
        getName.setAccessible(true);
        long start = System.currentTimeMillis();
        for (int i = 0; i < times; i++) {
            getName.invoke(teacher);
        }
        long end = System.currentTimeMillis();
        System.out.println("關閉安全檢查反射方式執行10億次消耗:" + (end - start) + "ms");
    }

}


class Teacher {

    private String name;

    public String getName() {
        return name;
    }

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

//輸出結果
普通方式執行10億次消耗:13ms
反射方式執行10億次消耗:20141ms
關閉安全檢查反射方式執行10億次消耗:8233ms

經過實驗可知,反射比直接執行相同的方法慢了不少,特別是當反射的操做被頻繁調用時效果更明顯,固然經過關閉安全檢查能夠提升一些速度。因此,放射也不該該氾濫成災的,而是適度使用才能發揮最大做用。

相關文章
相關標籤/搜索