【Java8新特性】重複註解與型別註解,你真的學會了嗎?

冰河團隊發表於2020-06-02

寫在前面

在Java8之前,在某個類或者方法,欄位或者引數上標註註解時,同一個註解只能標註一次。但是在Java8中,新增了重複註解和型別註解,也就是說,從Java8開始,支援在某個類或者方法,欄位或者引數上標註多個相同的註解。那麼,有讀者就會問了:如何實現呢?別急,往下看!文中不只是Java8中的註解。

JDK5中的註解

1.註解(@)

註解就相當於一種標記,在程式中加了註解就等於為程式加了某種標記。(JDK1.5新特性)。

2.作用

告訴javac編譯器或者java開發工具……向其傳遞某種資訊,作為一個標記。

3.如何理解註解?

一個註解就是一個類。

標記可以加在包、類、欄位、方法,方法引數以及區域性變數上。可以同時存在多個註解。

每一個註解結尾都沒有“;”或者其他特別符號。

定義註解需要的基礎註解資訊如下所示。

@SuppressWarnings("deprecation")  //編譯器警告過時(source階段)
@Deprecated						//過時(Runtime階段)
@Override						//重寫(source階段)
@Retention(RetentionPolicy.RUNTIME)	
//保留註解到程式執行時。(Runtime階段)
@Target({ElementType.METHOD,ElementType.TYPE})
//標記既能定義在方法上,又能定義在類、介面、列舉上等。

注意:

1)新增註解需要有註解類。RetentionPolicy是一個列舉類(有三個成員)。

2)Target中可以存放陣列。它的預設值為任何元素。

  • ElementType.METHOD:表示只能標記在方法上。
  • ElementType.TYPE:表示只能標記定義在類上、介面上、列舉上等

3)ElementType也是列舉類。成員包括:ANNOTATION_TYPE(註解)、CONSTRUCTOR(構造方法)、FIEID(成員變數)、LOCAL_VARIABLE(變數)、METHOD(方法)、PACKAGE(包)、PARAMETER(引數)、TYPE。

4.關於註解

  • 元註解:註解的註解(理解:給一個註解類再加註解)
  • 後設資料:資料的資料
  • 元資訊:資訊的資訊

5.註解分為三個階段

java原始檔--> class檔案 --> 記憶體中的位元組碼。

Retention的註解有三種取值:(分別對應註解的三個階段)

  • RetentionPolicy.SOURCE
  • RetentionPolicy.CLASS
  • RetentionPolicy.RUNTIME

注意:註解的預設階段是Class。

6.註解的屬性型別

原始型別(就是八個基本資料型別)、String型別、Class型別、陣列型別、列舉型別、註解型別。

7.為註解增加屬性

value:是一個特殊的屬性,若在設定值時只有一個value屬性需要設定或者其他屬性都採用預設值時 ,那麼value=可以省略,直接寫所設定的值即可。

例如:@SuppressWarnings("deprecation")

為屬性指定預設值(預設值):
例如:String value() default "blue"; //定義在註解類中

陣列型別的屬性:
例如:int[] arrayArr() default {3,4,5,5};//定義在註解類中
SunAnnotation(arrayArr={3,9,8}) //設定陣列值
注意:如果陣列屬性中只有一個元素時,屬性值部分可以省略大括號。
例如:SunAnnotation(arrayArr=9)

列舉型別的屬性:
例如:EnumDemo.TrafficLamp lamp()
////列舉型別屬性, 定義在註解類中,這裡使用了自定義的列舉類EnumDemo.java並沒有給出相關程式碼,這裡只是舉個例子
default EnumDemo.TrafficLamp.RED;

註解型別的屬性:
例如:MetaAnnotation annotationAttr()
//定義在一個註解類中,並指定預設值,
//此屬性關聯到註解類:MetaAnnotation.java, 
default @MetaAnnotation("lhm");
//設定註解屬性值
@SunAnnotation(annotationAttr=@MetaAnnotation("flx"))

Java8中的註解

對於註解(也被稱做後設資料),Java 8 主要有兩點改進:型別註解和重複註解。

1.型別註解

1)Java 8 的型別註解擴充套件了註解使用的範圍。

在java 8之前,註解只能是在宣告的地方所使用,java8開始,註解可以應用在任何地方。

例如:

建立類例項

new @Interned MyObject();

型別對映

myString = (@NonNull String) str;

implements 語句中

class UnmodifiableList<T> implements@Readonly List<@Readonly T> { ... }

throw exception宣告

void monitorTemperature() throws@Critical TemperatureException { ... }

注意:

在Java 8裡面,當型別轉化甚至分配新物件的時候,都可以在宣告變數或者引數的時候使用註解。
Java註解可以支援任意型別。

型別註解只是語法而不是語義,並不會影響java的編譯時間,載入時間,以及執行時間,也就是說,編譯成class檔案的時候並不包含型別註解。

2)新增ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上)

新增的兩個註釋的程式元素型別 ElementType.TYPE_USE 和 ElementType.TYPE_PARAMETER用來描述註解的新場合。

  • ElementType.TYPE_PARAMETER 表示該註解能寫在型別變數的宣告語句中。
  • ElementType.TYPE_USE 表示該註解能寫在使用型別的任何語句中(例如:宣告語句、泛型和強制轉換語句中的型別)。

例如,下面的示例。

@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}

3)型別註解的作用

型別註解被用來支援在Java的程式中做強型別檢查。配合第三方外掛工具Checker Framework(注:此外掛so easy,這裡不介紹了),可以在編譯的時候檢測出runtime error(例如:UnsupportedOperationException; NumberFormatException;NullPointerException異常等都是runtime error),以提高程式碼質量。這就是型別註解的作用。

注意:使用Checker Framework可以找到型別註解出現的地方並檢查。

例如下面的程式碼。

import checkers.nullness.quals.*;
public class TestDemo{
    void sample() {
        @NonNull Object my = new Object();
    }
}

使用javac編譯上面的類:(當然若下載了Checker Framework外掛就不需要這麼麻煩了)

javac -processor checkers.nullness.NullnessChecker TestDemo.java

上面編譯是通過的,但若修改程式碼:

@NonNull Object my = null;

但若不想使用型別註解檢測出來錯誤,則不需要processor,正常javac TestDemo.java是可以通過編譯的,但是執行時會報 NullPointerException 異常。

為了能在編譯期間就自動檢查出這類異常,可以通過型別註解結合 Checker Framework 提前排查出來錯誤異常。

注意java 5,6,7版本是不支援註解@NonNull,但checker framework 有個向下相容的解決方案,就是將型別註解@NonNull 用/**/註釋起來。

import checkers.nullness.quals.*;
public class TestDemo{
    void sample() {
        /*@NonNull*/ Object my = null;
    }
}

這樣javac編譯器就會忽略掉註釋塊,但用checker framework裡面的javac編譯器同樣能夠檢測出@NonNull錯誤。
通過 型別註解 + checker framework 可以在編譯時就找到runtime error。

2.重複註解

允許在同一宣告型別(類,屬性,或方法)上多次使用同一個註解。

Java8以前的版本使用註解有一個限制是相同的註解在同一位置只能使用一次,不能使用多次。

Java 8 引入了重複註解機制,這樣相同的註解可以在同一地方使用多次。重複註解機制本身必須用 @Repeatable 註解。

實際上,重複註解不是一個語言上的改變,只是編譯器層面的改動,技術層面仍然是一樣的。

例如,我們可以使用如下示例來具體對比Java8之前的版本和Java8中的註解。

1)自定義一個包裝類Hints註解用來放置一組具體的Hint註解

@interface MyHints {
    Hint[] value();
}
 
@Repeatable(MyHints.class)
@interface Hint {
    String value();
}

使用包裝類當容器來存多個註解(舊版本方法)

@MyHints({@Hint("hint1"), @Hint("hint2")})
class Person {}

使用多重註解(新方法)

@Hint("hint1")
@Hint("hint2")
class Person {}

2)完整類測試如下所示。

public class RepeatingAnnotations {
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Filters {
        Filter[] value();
    }
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Repeatable(Filters.class)
    public @interface Filter {
        String value();
    }
    @Filter("filter1")
    @Filter("filter2")
    public interface Filterable {
    }
    public static void main(String[] args) {
        for (Filter filter : Filterable.class.getAnnotationsByType(Filter.class)) {
            System.out.println(filter.value());
        }
    }
}

輸出結果:

filter1
filter2

分析:

註釋Filter被@Repeatable( Filters.class )註釋。Filters 只是一個容器,它持有Filter, 編譯器盡力向程式設計師隱藏它的存在。通過這樣的方式,Filterable介面可以被Filter註釋兩次。

另外,反射的API提供一個新方法getAnnotationsByType() 來返回重複註釋的型別(注意Filterable.class.getAnnotation( Filters.class )將會返回編譯器注入的Filters例項。

3)java 8之前也有重複使用註解的解決方案,但可讀性不好。

public @interface MyAnnotation {  
     String role();  
}  
 
public @interface Annotations {  
    MyAnnotation[] value();  
}  
 
public class RepeatAnnotationUseOldVersion {  
    @Annotations({@MyAnnotation(role="Admin"),@MyAnnotation(role="Manager")})  
    public void doSomeThing(){  
    }  
}

Java8的實現方式(由另一個註解來儲存重複註解,在使用時候,用儲存註解Authorities來擴充套件重複註解),可讀性更強。

@Repeatable(Annotations.class) 
public @interface MyAnnotation {  
     String role();  
}  
 
public @interface Annotations {  
    MyAnnotation[] value();  
}  
 
public class RepeatAnnotationUseOldVersion {  
	@MyAnnotation(role="Admin")  
    @MyAnnotation(role="Manager")
    public void doSomeThing(){  
    }  
} 

什麼?沒看懂?那就再來一波!!!

Java8對註解的增強

Java 8對註解處理提供了兩點改進:可重複的註解及可用於型別的註解。總體來說,比較簡單,下面,我們就以例項的形式來說明Java8中的重複註解和型別註解。

首先,我們來定義一個註解類BingheAnnotation,如下所示。

package io.mykit.binghe.java8.annotition;

import java.lang.annotation.*;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定義註解
 */
@Repeatable(BingheAnnotations.class)
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE,ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface BingheAnnotation {
    String value();
}

注意:在BingheAnnotation註解類上比普通的註解多了一個@Repeatable(BingheAnnotations.class)註解,有小夥伴會問:這個是啥啊?這個就是Java8中定義可重複註解的關鍵,至於BingheAnnotations.class,大家別急,繼續往下看就明白了。

接下來,我們們定義一個BingheAnnotations註解類,如下所示。

package io.mykit.binghe.java8.annotation;

import java.lang.annotation.*;

/**
 * @author binghe
 * @version 1.0.0
 * @description 定義註解
 */
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE,ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface BingheAnnotations {
    BingheAnnotation[] value();
}

看到這裡,大家明白了吧!!沒錯,BingheAnnotations也是一個註解類,它相比於BingheAnnotation註解類來說,少了一個@Repeatable(BingheAnnotations.class)註解,也就是說,BingheAnnotations註解類的定義與普通的註解幾乎沒啥區別。值得注意的是,我們在BingheAnnotations註解類中,定義了一個BingheAnnotation註解類的陣列,也就是說,在BingheAnnotations註解類中,包含有多個BingheAnnotation註解。所以,在BingheAnnotation註解類上指定@Repeatable(BingheAnnotations.class)來說明可以在類、欄位、方法、引數、構造方法、引數上重複使用BingheAnnotation註解。

接下來,我們建立一個Binghe類,在Binghe類中定義一個init()方法,在init方法上,重複使用@BingheAnnotation註解指定相應的資料,如下所示。

package io.mykit.binghe.java8.annotation;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試註解
 */
@BingheAnnotation("binghe")
@BingheAnnotation("class")
public class Binghe {

    @BingheAnnotation("init")
    @BingheAnnotation("method")
    public void init(){

    }
}

到此,我們就可以測試重複註解了,建立類BingheAnnotationTest,對重複註解進行測試,如下所示。

package io.mykit.binghe.java8.annotation;

import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @author binghe
 * @version 1.0.0
 * @description 測試註解
 */
public class BingheAnnotationTest {

    public static void main(String[] args) throws NoSuchMethodException {
        Class<Binghe> clazz = Binghe.class;
        BingheAnnotation[] annotations = clazz.getAnnotationsByType(BingheAnnotation.class);
        System.out.println("類上的重複註解如下:");
        Arrays.stream(annotations).forEach((a) -> System.out.print(a.value() + " "));

        System.out.println();
        System.out.println("=============================");

        Method method = clazz.getMethod("init");
        annotations = method.getAnnotationsByType(BingheAnnotation.class);
        System.out.println("方法上的重複註解如下:");
        Arrays.stream(annotations).forEach((a) -> System.out.print(a.value() + " "));
    }
}

執行main()方法,輸出如下的結果資訊。

類上的重複註解如下:
binghe class 
=============================
方法上的重複註解如下:
init method 

寫在最後

如果覺得文章對你有點幫助,請微信搜尋並關注「 冰河技術 」微信公眾號,跟冰河學習Java8新特性。

最後,附上Java8新特性核心知識圖,祝大家在學習Java8新特性時少走彎路。

相關文章