1. 程式人生 > >淺談java註解<最通俗易懂的講解>

淺談java註解<最通俗易懂的講解>

Java註解用於為Java程式碼提供元資料。

元資料是指用來描述資料的資料,通俗一點,就是描述程式碼間關係,或者程式碼與其它資源(例如資料庫表)之間內在聯絡的資料。在一些技術框架中,如Struts、hibernate就不知不覺用到了元資料。對於Struts來說,元資料指的是struts-config.xml;對hibernate來說就是hbm檔案。以上闡述的幾種元資料都是基於xml檔案的或者其他形式的單獨配置檔案。這樣表示有些不便之處。1、與被描述的檔案分離,不利於一致性的維護;2、所有這樣的檔案都是ASCII檔案,沒有顯式的型別支援。基於元資料的廣泛使用,JDK5.0引入了Annotation的概念來描述元資料。在Java中,元資料以標籤的形式存在於Java程式碼中,元資料標籤的存在並不影響程式程式碼的編譯和執行。簡而言之,言而總之,註解就是標籤的意思。

一、如何建立註解?

JDK5.0出來後,Java語言中就有了四種類型,即類class、列舉enum、介面interface、註解@interface,它們處於同一級別,Java就是通過註解來表示元資料的。

package OSChina.ClientNew;

public @interface MyAnnotation {
    // 定義公共的final靜態屬性
    int age = 25;

    // 定義公共的抽象方法
    String name();
}

Java註解本質上就是介面,是繼承了Annotation介面的介面。

二、元註解

元註解是可以註解到註解上的註解,或者說元註解是一種基本註解,它能夠應用到其它的註解上面。

元標籤有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 種。

1、@Retention

Retention,中文釋義保留期的意思

當@Retention應用到註解上的時候,它解釋說明了這個註解的生命週期。

  • RetentionPolicy.SOURCE 註解只在原始碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
  • RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被載入到JVM中。
  • RetentionPolicy.RUNTIME 註解可以保留到程式執行的時候,它會被載入到JVM中。

2、@Documented

顧名思義,這個元註解肯定和文件有關。它的作用是能夠將註解中的元素包含到Javadoc中去。

3、@Target

標明註解運用的地方。

  • ElementType.ANNOTATION_TYPE 可以給一個註解進行註解
  • ElementType.CONSTRUCTOR 可以給構造方法進行註解
  • ElementType.FIELD 可以給屬性進行註解
  • ElementType.LOCAL_VARIABLE 可以給區域性變數進行註解
  • ElementType.METHOD 可以給方法進行註解
  • ElementType.PACKAGE 可以給一個包進行註解
  • ElementType.PARAMETER 可以給一個方法內的引數進行註解
  • ElementType.TYPE 可以給一個型別進行註解,比如類、介面、列舉

4、@Inherited

lnherited是繼承的意思。

如果一個超類被@Inherited註解過的註解進行註解的話,那麼如果它的子類沒有被任何註解應用的話,那麼這個子類就繼承了超類的註解。

程式碼例項

package OSChina.ClientNew;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)//註解可以保留到程式執行時,載入到JVM中
@Target(ElementType.TYPE)//給一個型別進行註解,比如類、介面、列舉
@Inherited //子類繼承父類時,註解會起作用
public @interface Desc {
    enum Color {
        White, Grayish, Yellow
    }

    // 預設顏色是白色的
    Color c() default Color.White;
}

5、@Repeatable

Repeatable 自然是可重複的意思。@Repeatable 是 Java 1.8 才加進來的,所以算是一個新的特性。

什麼樣的註解會多次應用呢?通常是註解的值可以同時取多個。

在生活中一個人往往是具有多種身份,如果我把每種身份當成一種註解該如何使用???

先宣告一個Persons類用來包含所有的身份

@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {
	Person[] value();
}

這裡@Target是宣告Persons註解的作用範圍,引數ElementType.Type代表可以給一個型別進行註解,比如類,介面,列舉。

@Retention是註解的有效時間,RetentionPolicy.RUNTIME是指程式執行的時候。

Person註解:

@Repeatable(Persons.class)
public @interface Person{
	String role() default "";
}

@Repeatable括號內的就相當於用來儲存該註解內容的容器。

宣告一個Man類,給該類加上一些身份。

@Person(role="CEO")
@Person(role="husband")
@Person(role="father")
@Person(role="son")
public class Man {
	String name="";
}

在主方法中訪問該註解:

public static void main(String[] args) {
    Annotation[] annotations = Man.class.getAnnotations();  
    System.out.println(annotations.length);
    Persons p1=(Persons) annotations[0];
    for(Person t:p1.value()){
        System.out.println(t.role());
    }
}

下面的程式碼結果輸出相同,但是可以先判斷是否是相應的註解,比較嚴謹。 

if(Man.class.isAnnotationPresent(Persons.class)) {
    Persons p2=Man.class.getAnnotation(Persons.class);
    for(Person t:p2.value()){
        System.out.println(t.role());
    }
 }

執行結果:

三、註解的屬性

註解的屬性也叫做成員變數,註解只有成員變數,沒有方法。註解的成員變數在註解的定義中以“無參的方法”形式來宣告,其方法名定義了該成員變數的名字,其返回值定義了該成員變數的型別。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    int id();
    String msg();
}

上面程式碼中定義了TestAnnotation這個註解中擁有id和msg兩個屬性。在使用的時候,我們應該給他們進行賦值。

賦值的方式是在註解的括號內以value=“”形式,多個屬性之前用,隔開。

@TestAnnotation(id=3,msg="hello annotation")
public class Test {
}

需要注意的是,在註解中定義屬性時它的型別必須是 8 種基本資料型別外加 類、介面、註解及它們的陣列。

註解中屬性可以有預設值,預設值需要用 default 關鍵值指定。比如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    public int id() default -1;
    public String msg() default "江疏影";
}

TestAnnotation 中 id 屬性預設值為 -1,msg 屬性預設值為 江疏影。 

它可以這樣應用。

@TestAnnotation()
public class Test {}

因為有預設值,所以無需要再在 @TestAnnotation 後面的括號裡面進行賦值了,這一步可以省略。

另外,還有一種情況。如果一個註解內僅僅只有一個名字為 value 的屬性時,應用這個註解時可以直接接屬性值填寫到括號內。

public @interface Check {
    String value();
}

上面程式碼中,Check 這個註解只有 value 這個屬性。所以可以這樣應用。

@Check("hi")
int a;

這和下面的效果是一樣的

@Check(value="hi")
int a;

最後,還需要注意的一種情況是一個註解沒有任何屬性。比如

public @interface Perform {}

那麼在應用這個註解的時候,括號都可以省略。

@Perform
public void testMethod(){}

四、Java預置的註解

學習了上面相關的知識,我們已經可以自己定義一個註解了。其實 Java 語言本身已經提供了幾個現成的註解。

1、@Override

這個大家應該很熟悉了,提示子類要複寫父類中被 @Override 修飾的方法

2、@Deprecated

加上這個註解之後,表示此方法或類不再建議使用,呼叫時會出現刪除線,但不代表不能用,只是說,不推薦使用,因為有更好的方法可以呼叫。

那麼直接刪掉不就完了?

因為在一個專案中,工程比較大,程式碼比較多,而在後續的開發過程中,可能之前的某個方法實現的並不是很合理,這個時候要重新寫一個方法,而之前的方法還不能隨便刪,因為別的地方可能在呼叫它,所以加上這個註解,就OK啦!

package OSChina.ClientNew;

import java.util.ArrayList;
import java.util.List;

public class Hero {
    @Deprecated
    public void say(){
        System.out.println("nothing has to say!");
    }
    public void speak(){
        System.out.println("i have a dream!");
    }

    public void  addItems(String item){
        List items =  new  ArrayList();
        items.add(item);
        System.out.println("i am "+items);
    }
}

3、@SuppressWarnings

阻止警告的意思。

該批註的作用是給編譯器一條指令,告訴它對被批註的程式碼元素內部的某些警告保持靜默。

注:這個註解有很多引數,這裡就不多做贅述了,如有需要,請自行百度!

4、@SafeVarargs

引數安全型別註解。

它的目的是提醒開發者不要用引數做一些不安全的操作,它的存在會阻止編譯器產生unchecked這樣的警告。

在宣告具有模糊型別(比如:泛型)的可變引數的建構函式或方法時,Java編譯器會報unchecked警告。鑑於這種情況,如果程式猿斷定宣告的建構函式和方法的主體no problem,可使用@SafeVarargs進行標記,這樣Java編譯器就不會報unchecked警告了!

先看看@SafeVarargs在Java SE中的宣告:

package java.lang;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs {}

由Java原始碼宣告我們瞭解到:@SafeVarargs註解,只能用於標記建構函式和方法,由於保留策略宣告為RUNTIME,所以此註解可以在執行時生效。

@SafeVarargs註解,只能用於static或final的方法。

程式碼例項:

泛型引數的方法,不加註解的情況:

package OSChina.ClientNew;

public class SafeVarargsAnnotation<S> {
    private S[] args;
    public SafeVarargsAnnotation(S... args){
        this.args = args;
    }
    
    public void loopPrintArgs(S... args){
        for (S arg : args){
            System.out.println(arg);
        }
    }
    
    public final void printSelfArgs(S... args){
        for (S arg : this.args) {
            System.out.println(arg);
        }
    }
    
    public static <T> void loopPrintInfo(T... infos){
        for(T info:infos){
            System.out.println(info);
        }
    }

    public static void main(String[] args) {
        SafeVarargsAnnotation.loopPrintInfo("A","B","C");
    }
}

註解的正確使用方式:

package OSChina.ClientNew;

public class SafeVarargsAnnotation<S> {
    private S[] args;
    //建構函式可以使用@SafeVarargs標記
    @SafeVarargs
    public SafeVarargsAnnotation(S... args){
        this.args = args;
    }

    //此處不能使用@SafeVarargs,因為此方法未宣告為static或final方法,
    // 如果要抑制unchecked警告,可以使用@SuppressWarnings註解
    @SuppressWarnings("unchecked")
    public void loopPrintArgs(S... args){
        for (S arg : args){
            System.out.println(arg);
        }
    }
    //final方法可以使用@SafeVarargs標記
    @SafeVarargs
    public final void printSelfArgs(S... args){
        for (S arg : this.args) {
            System.out.println(arg);
        }
    }
    //static方法可以使用@SafeVarargs標記
    @SafeVarargs
    public static <T> void loopPrintInfo(T... infos){
        for(T info:infos){
            System.out.println(info);
        }
    }

    public static void main(String[] args) {
        SafeVarargsAnnotation.loopPrintInfo("A","B","C");
    }
}

5、@FunctionalInterface

Java 8為函式式介面引入了一個新註解@FunctionalInterface,主要用於編譯級錯誤檢查,加上該註解,當你寫的介面不符合函式式介面定義的時候,編譯器會報錯。

它們主要用在Lambda表示式和方法引用(實際上也可認為是Lambda表示式)上。

如定義了一個函式式介面如下:

@FunctionalInterface
interface GreetingService 
{
    void sayMessage(String message);
}

那麼就可以使用Lambda表示式來表示該介面的一個實現(注:JAVA 8 之前一般是用匿名類實現的):

GreetingService greetService1 = message -> System.out.println("Hello " + message);

淺談lambda表示式<最通俗易懂的講解>

五、註解與反射

1、註解通過反射獲取。首先可以通過 Class 物件的 isAnnotationPresent() 方法判斷它是否應用了某個註解。

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

2、或者是 getAnnotations() 方法。

public Annotation[] getAnnotations() {}

前一種方法返回指定型別的註解,後一種方法返回註解到這個元素上的所有註解。

3、程式碼例項:

① 沒加註解的時候:

package OSChina.ClinetNew1.Annotation;

public class Test {
    public static void main(String[] args) {
        boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
        if(hasAnnotation){
            TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
            System.out.println("id:"+testAnnotation.id());
            System.out.println("msg:"+testAnnotation.msg());
        }
    }
}

屁都沒有!

② 加上註解

package OSChina.ClinetNew1.Annotation;

@TestAnnotation
public class Test {
    public static void main(String[] args) {
        boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
        if(hasAnnotation){
            TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
            System.out.println("id:"+testAnnotation.id());
            System.out.println("msg:"+testAnnotation.msg());
        }
    }
}

這個正是 TestAnnotation 中 id 和 msg 的預設值。

上面的例子只是檢閱出了註解在類上的註解,其實屬性、方法上的註解也是一樣的。同樣還是要假手與反射。

③ 屬性和方法上的註解:

package OSChina.ClinetNew1.Annotation;

public @interface Check {
    String value();
}
package OSChina.ClinetNew1.Annotation;

public @interface Perform {
}
package OSChina.ClinetNew1.Annotation;

import OSChina.ClientNew.Hero;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

@TestAnnotation(msg="hello")
public class Test {
    @Check(value="hi")
    int a;
    @Perform
    public void testMethod(){}
    @SuppressWarnings("deprecation")
    public void test1(){
        Hero hero = new Hero();
        hero.say();
        hero.speak();
    }
    public static void main(String[] args) {
        boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);
        if ( hasAnnotation ) {
            TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
            //獲取類的註解
            System.out.println("id:"+testAnnotation.id());
            System.out.println("msg:"+testAnnotation.msg());
        }
        try {
            Field a = Test.class.getDeclaredField("a");
            a.setAccessible(true);
            //獲取一個成員變數上的註解
            Check check = a.getAnnotation(Check.class);
            if ( check != null ) {
                System.out.println("check value:"+check.value());
            }
            Method testMethod = Test.class.getDeclaredMethod("testMethod");
            if ( testMethod != null ) {
                // 獲取方法中的註解
                Annotation[] ans = testMethod.getAnnotations();
                for( int i = 0;i < ans.length;i++) {
                    System.out.println("method testMethod annotation:"+ans[i].annotationType().getSimpleName());
                }
            }
        } catch (NoSuchFieldException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println(e.getMessage());
        } catch (SecurityException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println(e.getMessage());
        } catch (NoSuchMethodException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            System.out.println(e.getMessage());
        }
    }
}

需要注意的是,如果一個註解要在執行時被成功提取,那麼 @Retention(RetentionPolicy.RUNTIME) 是必須的。

六、註解的使用場景

1、註解的官方釋義:

註解是一系列元資料,它提供資料用來解釋程式程式碼,但是註解並非是所解釋的程式碼本身的一部分。註解對於程式碼的執行效果沒有直接影響。

2、註解有許多用處:

① 提供資訊給編譯器:編譯器可以利用註解來探測錯誤或警告資訊

② 編譯階段時的處理:軟體工具可以利用註解資訊來生成程式碼、HTML文件或其它響應處理。

③ 執行時的處理:某些註解可以在程式執行時接受程式碼的提取。

值得注意的是,註解不是程式碼本身的一部分。

3、註解運用的地方太多了,比如JUnit測試框架,典型的使用方法:

public class ExampleUnitTest {
    @Test
    public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
}

@Test 標記了要進行測試的方法 addition_isCorrect().

還有例如ssm框架等運用了大量的註解。

七、註解的應用例項

package OSChina.ClientNew;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface Desc {
    enum Color {
        White, Grayish, Yellow
    }

    // 預設顏色是白色的
    Color c() default Color.White;
}

該註解Desc前增加了三個註解:Retention表示的是該註解的保留級別,Target表示的是註解可以標註在什麼地方,@Inherited表示該註解會被自動繼承。

package OSChina.ClinetNew1;

@Desc(c = Desc.Color.White)
abstract class Bird {
    public abstract Desc.Color getColor();
}
package OSChina.ClinetNew1;

public enum BirdNest {
    Sparrow;
    // 鳥類繁殖
    public Bird reproduce() {
        Desc bd = Sparrow.class.getAnnotation(Desc.class);
        return bd == null ? new Sparrow() : new Sparrow(bd.c());
    }
}
package OSChina.ClinetNew1;

public class Sparrow extends Bird {
    private Desc.Color color;
    // 預設是淺灰色
    public Sparrow() {
        color = Desc.Color.Grayish;
    }

    // 建構函式定義鳥的顏色
    public Sparrow(Desc.Color _color) {
        color = _color;
    }

    @Override
    public Desc.Color getColor() {
        return color;
    }
}

上面程式聲明瞭一個Bird抽象類,並且標註了Desc註解,描述鳥類的顏色是白色,然後編寫一個麻雀Sparrow類,它有兩個建構函式,一個是預設的建構函式,也就是我們經常看到的麻雀是淺灰色的,另外一個建構函式是自定義麻雀的顏色,之後又定義了一個鳥巢(工廠方法模式),它是專門負責鳥類繁殖的,它的生產方法reproduce會根據實現類註解資訊生成不同顏色的麻雀。我們編寫一個客戶端呼叫,程式碼如下:   

public static void main(String[] args) {
    Bird bird = BirdNest.Sparrow.reproduce();
    Desc.Color color = bird.getColor();
    System.out.println("Bird's color is :" + color);
}

會打印出什麼呢?因為採用了工廠方法模式,它主要的問題是bird比那裡到底採用了哪個建構函式來生成,如果單獨看子類Sparrow,它沒有任何註釋,那工廠方法中bd變數應該就是null了,應該呼叫無參構造!

輸出為什麼會是白色呢?這是我們新增到父類的顏色,why?這是因為我們在註解上加了@Inherited註解,它表示的意思是我們只要把註解@Desc加到父類Bird上,它的所有子類都會從父類繼承@Desc註解。

八、總結

1、註解就是標籤,註解為了解釋程式碼

2、註解的基本語法@interface

3、註解的元註解

4、註解的屬性

5、註解主要給編譯器及工具型別的軟體用的

6、註解的提取要藉助於Java的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本

 

江疏