夯實 Java 基礎 - 註解

不知道你們有沒有一種感受,當你想要了解某個知識點的時候,就會發現好多技術類 APP 或者公衆號在推一些關於這個知識點的文章。也許這就是大數據的做用,這也說明總有人比你搶先一步。學習不能停滯,要不你就會被別人越落越遠。java

本文接着來回顧和總結 Java 基礎中註解的知識點和簡單的使用,一樣本文將從如下幾個方面來回顧註解知識:android

  1. 註解的定義
  2. 註解的語法
  3. 源碼級別的註解的使用
  4. 運行時註解的使用
  5. 編譯時註解的使用
  6. Android 預置的註解

註解的定義

註解(Annotation),也叫元數據。一種代碼級別的說明。它是 JDK 1.5 之後版本引入的一個特性,與類、接口、枚舉是在同一個層次。它能夠聲明在包、類、字段、方法、局部變量、方法參數等元素上。它提供數據用來解釋程序代碼,可是註解並不是是所解釋的代碼自己的一部分。註解對於代碼的運行效果沒有直接影響數組

註解有許多用處,主要以下:sass

  • 提供信息給編譯器: 編譯器能夠利用註解來探測錯誤和警告信息
  • 編譯階段時的處理: 軟件工具能夠用來利用註解信息來生成代碼、Html 文檔或者作其它相應處理。
  • 運行時的處理: 某些註解能夠在程序運行的時候接受代碼的提取

如咱們所熟知的依賴注入框架 ButterKnife 就是在編譯階段來生成 findViewById 的代碼(文件)的,而咱們所見過的 @Deprecated 就是提供信息給編輯器的RetentionPolicy.SOURCE類型註解,說明這個屬性已通過時的,對於運行時的註解在反射的文章的最後咱們也舉了個小例子,說明了它的做用。app

在自定義了一個編譯或者運行階段的註解後,須要一個開發者編寫相應的代碼來解釋這些註解,從而來發揮註解的做用。這些用來解釋註解的代碼被統稱爲是 APT(Annotation Processing Tool)。換句話說註解實際上是給 APT 或者編輯器來使用的,而對於非框架開發人員的咱們咱們只須要關注註解的使用,並遵照規則便可,從而咱們節省了不少代碼提升了效率。框架

可是凡事若是隻知足於用上,就不算是一個合 (tong) 格 (guo) 程 (mian)序 (shi) 員 (de)! 可是不要慌,當你打開這篇文章的時候你已經離 offer 又進了一步。編輯器

註解的語法

註解的聲明

註解的聲明和聲明一個接口十分相似,沒錯只是名字很相似~ 咱們使用@interface 來聲明一個註解,如咱們最多見的Override 註解的聲明ide

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
複製代碼

註解聲明的修飾符,能夠是 private,public, protected 或者默認 default 這一點跟定義一個類或者接口相同。函數

在聲明一個註解的時候咱們經常須要一些其餘註解來修飾和限制該定義註解的使用和運行方式。上述的 @Target@Retention 就是如此,咱們稱之爲元註解,詳細的元註解在下邊說明。工具

註解成員

註解跟一個類類似,它們並非都是像上面的 @Override同樣只有聲明。一個類大概能夠包含構造函數,成員變量,成員函數等,而一個註解只能包含註解成員,註解成員的聲明格式爲:

類型 參數名() default 默認值;

註解成員能夠是:

  1. 基本類型 byte,short,int,long,float,double,boolean 八種基本類型及這些類型的數組, 注意這裏沒對應基本數據類型的包裝類。

  2. String,Enum,Class,annotations 及這些類型的數組

  3. 註解的成員修飾符只能是 public 或默認(default)

  4. 註解元素必須有肯定的值,能夠在註解中定義默認值,也可使用註解時指定。即咱們在定義註解的時候聲明的成員,能夠不賦值,可是就跟抽象函數同樣,在使用的時候就必須指定。

如:

public @interface TestAnnotation {

   String value() default "";

   String[] values();

   int id() default -1;

   int[] ids();

   // 錯誤的不能使用包裝類 以及自定義類型
   // Integer idInt();
   // Apple apple();

   enum Color {BULE, RED, GREEN}
   Color testEnum() default Color.BULE;
   Color[] testEnums();
   
   //註解類型成員 註解元素必須有肯定的值,能夠在註解中定義默認值,也可使用註解時指定
   FruitName fruitName() default @FruitName("apple");
}
    
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
protected @interface FruitName {
   String value();
   String alias() default "no alias";
}
複製代碼

元註解

咱們在 Override 註解的聲明中能夠看到還有註解修飾着如@Target(ElementType.METHOD),咱們講元註解理解爲修飾註解定義的註解。換句話說元註解爲 JDK 提供給咱們的一些基本註解,咱們使用元註解來定義一個註解是如何工做的。

JDK 1.8 中存在的元註解有如下 5 種:

@Target, @Retention、@Documented、@Inherited、@Repeatable

下面咱們依次來講明這幾種類型的註解是如何使用的。

@Target 元註解

@Target 指定了被修飾的註解運用的地方,這些 "地方" 定義在 ElementType 類中,包括:

  1. ElementType.ANNOTATION_TYPE 能夠給一個註解進行註解
  2. ElementType.CONSTRUCTOR 能夠給構造方法進行註解
  3. ElementType.FIELD 能夠給屬性進行註解
  4. ElementType.LOCAL_VARIABLE 能夠給局部變量進行註解
  5. ElementType.METHOD 能夠給方法進行註解
  6. ElementType.PACKAGE 能夠給一個包進行註解
  7. ElementType.PARAMETER 能夠給一個方法內的參數進行註解
  8. ElementType.TYPE 能夠給一個類型進行註解,好比類、接口、枚舉

其中 METHODPARAMETERFIELD 最爲常見,如 Override 註解被 @Target(ElementType.METHOD) 修飾,若是咱們想要標記一個參數不能爲空則可使用 @NonNull 去修飾一個 param, FIELD 用來指定註解只能用來修飾成員變量如咱們常用的 @BindView

值得注意的是 @Target 元註解定義以下,

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}
複製代碼

它內部的成員爲ElementType[] 數組也就是說,咱們能夠同時指定一個註解能夠用於不少地方。如 @ColorRes 的註解的元註解爲@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})

@Retention 元註解

Retention 翻譯過來是保留期的意思。當@Retention 用於修飾一個註解上的時候,它規定了了被修飾的註解應用的時期,或者存活的時期

它能夠有以下 3 種取值:

  • RetentionPolicy.SOURCE 註解只在源碼階段保留,在編譯器進行編譯時它將被丟棄。
  • RetentionPolicy.RUNTIME 註解能夠保留到程序運行的時候,它會被加載進入到 JVM 中,因此在程序運行時經過反射獲取到它們,並解釋他們。
  • RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被加載到 JVM 中。

源碼級別註解 RetentionPolicy.SOURCE

對於第一種 RetentionPolicy.SOURCE 註解只在源碼階段保留,更多的效果時作一些編譯檢查,在 Android 中有個爲 @IntDef 的註解,他能夠和常量組合一塊兒 代替枚舉 enum 作參數限制做用,來優化內存使用。

這裏說只是替代了參數限制做用,而 JDK 1.5 爲咱們帶來的 enum 的做用不僅是簡單的參數限制做用做用,對於 Enum 更多優雅使用能夠參考 《Effective Java》。

@IntDef 的註解定義以下:

@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    long[] value() default {};
    boolean flag() default false;
}
複製代碼

如咱們經常使用的設置一個 View 的可見屬性就使用了 @IntDef 註解來保證使用者傳入的參數是對的,以下:

@IntDef({VISIBLE, INVISIBLE, GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface Visibility {}

@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
		setFlags(visibility, VISIBILITY_MASK);
}

//設置一個 View 的屬性:
...
toolbar.setVisibility(View.VISIBLE);// it is Ok

//toolbar.setVisibility(1000);// 若是咱們隨便寫一個數值 那麼編輯器將會報錯 
複製代碼

運行期時的註解 RetentionPolicy.RUNTIME

源碼級別的註解對咱們的編碼約束,運行期註解與之不一樣的是,若是要是讓該註解生效,咱們必需要編寫必定的代碼去將定義好的註解,在運行中"注入"應用中,看到運行時注入就能夠應該能想得起反射,是的注入這個操做就是須要開發人員本身編寫的。

另外,咱們也都瞭解,在運行反射的時候效率是沒法保證的。由於反射將遍歷對應類的 Class 文件來獲取相應的信息。因此運行時註解,並非那麼普遍被運用,而稍後咱們要說明的編譯期註解則不會對程序的運行形成效率的影響,所以應用更普遍一些。

咱們來試着寫一個 Dota 英雄名稱的運行期註解來了解下他的運做方式:

/**
* 定義一個註解表示英雄的名字
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
private @interface HeroName {
   String value();

   String alias();
}

/**
* 定義一個類包含英雄名稱的屬性
*/
public class Hero {
    // 定義註解的時候沒有 deflaut 屬性名稱因此在使用的時候必須賦值
   @HeroName(value = "Spirit Walker", alias = "SB")
   private String heroName;

   public void setHeroName(String heroName) {
       this.heroName = heroName;
   }

   public String getHeroName() {
       return heroName;
   }
}
複製代碼

ok 聲明就是這麼簡單,那麼如何讓一個屬性生效呢,這時候咱們就須要一個註解處理方法。爲了方便觀察運行註解的結果,因此咱們這個處理方法選擇傳遞一個 Hero 對象,不過你爲了更通用也能夠不用這麼作。

/** 運行時註解處理方法*/
public static void getHeroNameInfo(Hero hero) {
   try {
       Class<? extends Hero> clazz = hero.getClass();
       Field field = clazz.getDeclaredField("heroName");
       // Field isAnnotationPresent 判斷一個屬性是否被對應的註解修飾
       if (field.isAnnotationPresent(HeroName.class)) {
           //field.getAnnotation 獲取屬性的註解
           HeroName fruitNameAnno = field.getAnnotation(HeroName.class);
           hero.setHeroName("name = " +fruitNameAnno.value() +" alias = " + fruitNameAnno.alias());
       }
   } catch (NoSuchFieldException e) {
       e.printStackTrace();
   }
}
複製代碼

下面咱們來運行下程序測試下:

public static void main(String[] args) {
   Hero hero = new Hero();
   getHeroNameInfo(hero);
   System.out.println("hero = " + hero);
}
複製代碼

運行結果:

hero = Hero{heroName='name = Spirit Walker alias = SB'}
複製代碼

經過上述的例子,能夠了解運行時註解就是這樣聲明和運用的。相信 SB 這個別名更容易讓你們記得這個例子(白牛這個英雄其實很好玩,只是別名...)。

編譯時期的註解 RetentionPolicy.CLASS

通過運行時註解的瞭解,相比對於註解應該都有一個大概的瞭解了。接下來到了編譯時註解,這個註解類型,即是衆多工具庫中應用的註解類型,它不會影響運行時的效率問題,而是在編譯期,或者打包過程當中就生成了對應的代碼,在運行時將會生效。如咱們常見的 ButterKnifeEventBus

編譯時註解與運行時註解不一樣,編譯時註解主要是幫助咱們在編譯器編譯期使用註解處理器生成相應的代碼,幫咱們解放勞動力。

咱們知道運行時註解是經過反射來解釋對應註解並使註解生效的,那麼編譯時如何解釋對應的註解呢?這裏就須要用到註解處理器的知識了。

註解處理器(Annotation Processor)是javac的一個工具,它用來在編譯時掃描和處理註解(Annotation)。你能夠自定義註解,並註冊相應的註解處理器(自定義的註解處理器需繼承自AbstractProcessor)。

Java 中提供給咱們了註解處理器實現方法,主要是經過實現一個名爲 AbstractProcessor 的註解處理器基類。該抽象類要求咱們必須實現 process 方法來定義處理邏輯。下邊咱們來看下註解處理器中的幾個方法的做用:

 

public class NameProcessor extends AbstractProcessor {
    
    //會被註解處理工具調用,並輸入ProcessingEnviroment參數。ProcessingEnviroment提供不少有用的工具類如Elements, Types和Filer等
    @Override
    public synchronized void init(ProcessingEnvironment env){ }
    
   //返回最高所支持的java版本, 如返回 SourceVersion.latestSupported();
    @Override
    public SourceVersion getSupportedSourceVersion() { }
    
    //一個註解處理器可能會處理多個註解邏輯,這個方法將返回待處理的註解類型集合,返回值做爲參數傳遞給 process 方法。
    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    //process 函數就是咱們處理待處理註解的地方了,咱們須要在這裏編寫生成 java 文件的具體邏輯。 方法返回布爾值類型,表示註解是否已經處理完成。通常狀況下咱們返回 true 便可。
    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
複製代碼

註解處理器的處理步驟主要有如下:

  1. 編譯器開始執行註解處理器
  2. process 方法中循環處理註解元素(Element),找到被該註解所修飾的類,方法,或者屬性
  3. 拿上一步獲得的備註註解修飾的類或者屬性,方法,生成對應的輔助類,並寫入 java 文件
  4. 生成 java 文件後就能夠在運行時,在程序中獲取並調用對應的輔助方法,如 ButterKnife.bind(this); 方法就是獲取對應 Activity 的註解處理器生成的java 文件,並執行了構造函數。

自定義一個編譯時註解

自定義編譯時註解要比運行時註解要繁瑣一些。下面咱們來舉一個簡單的例子,意在說明編譯時註解是如何工做的。

在 Android 中爲了實現一個編譯時註解咱們通常須要藉助兩個三方庫:

  1. com.google.auto.service:auto-service:1.0-rc2 這是谷歌官方提供的一個註解處理註冊插件能夠幫助咱們更方便的註冊註解處理器,只須要在自定義的 Processor 類上方添加@AutoService(Processor.class)便可,不用本身動手執行註解處理器的註冊工做(即編寫 resource/META-INF/services/javax.annotation.processing.Processor文件)。

  2. 爲了更方便的在 process 文件中生成 Java 類,須要依賴一個 Square 公司開源的 javapoet 庫,com.squareup:javapoet:1.9.0 這個庫中包裝提供了一些好用的 API 幫助咱們更快更準確的構建 .java 文件。固然你也能夠本身手寫拼接字符串而後寫入文件(若是你能保證正確)。

仿照 ButterKnife 的實現,咱們創建一個新的 Android project ,而後建立兩個 Java Moudle,其中 processor 用來存放註解處理器,processor-lib 用來存放對應的註解,以下圖所示:

 

WX20180513-110801@2x.png

 

在註解處理器存在的lib的 build.gradle 中添加依賴關係:

 

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':processor-lib')
    compile 'com.squareup:javapoet:1.9.0'
    compile 'com.google.auto.service:auto-service:1.0-rc3'
}
複製代碼

主 moudle 中也須要添加對 processor 和 processor -lib 的依賴:

dependencies {
    ....
   implementation project(':processor-lib')
   // 注意這裏的註解處理器的依賴方式
   annotationProcessor project(':processor')
}
複製代碼

好了通過上述的準備咱們終於可以編寫咱們的編譯時註解了:

  1. 在 processor-lib 定義一個 Name 註解以下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Name {
    String name();
    String alias();
}
複製代碼
  1. 編寫兩個類使用咱們定義的註解:
public class SBHero {
    @Name(name = "Spirit Walker", alias = "SB")
    private String heroName;
}

public class PAHero {
    @Name(name = "Phantom Assassin", alias = "PA")
    private String heroName;
}
複製代碼
  1. 在 processor 註解處理lib 下定義一個 NamePorcessor
// @AutoService(Processor.class) 幫助咱們生成對應的註解處理器配置
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.wangshijia.www.processor.Name")
public class NamePorcessor extends AbstractProcessor {
    //文件寫入工具類
    private Filer filer;
    //能夠幫助咱們在 gradle 控制檯打印信息的類
    private Messager messager;
    // 元素操做的輔助類
    private Elements elementUtils;
    //自定義文件名的後綴
    private static final String SUFFIX = "AutoGenerate";

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * @return 你所須要處理的全部註解,該方法的返回值會被 process()方法所接收, 這裏其實只有Name 註解,
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(Name.class.getCanonicalName());
        return set;
    }
    ... 
}
複製代碼
  1. 最後咱們要編輯咱們的 process 方法了,process 方法中一共進行了下面這幾件事:

    • 遍歷程序中全部被該註解修飾器處理註解修飾的元素 存放進建立的Map集合
    • 依次取出map 中的元素構建對應的類和方法
    • 構建對應的方法內容
    • 生成.java 文件 位置在 ~/app/build/generated/source/apt 目錄下
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

   String packageName= "";
   // 得到被該註解聲明的元素
   Set<? extends Element> elememts = roundEnv.getElementsAnnotatedWith(Name.class);
   // 聲明一個存放成員變量的列表
   List<VariableElement> fields;
   //key 對應包含註解修飾元素的類的全類名 vaule 表明全部被註解修飾的變量
   Map<String, List<VariableElement>> maps = new HashMap<>();
   
   // 遍歷程序中全部被該註解修飾器處理註解修飾的元素
   for (Element ele : elememts) {
       //  ele.getKind() 獲取註解修飾的成員的類型,判斷該元素是否爲成員變量
       if (ele.getKind() == ElementKind.FIELD) {
           VariableElement varELe = (VariableElement) ele;
           // 獲取該元素封裝類型
           TypeElement enclosingElement = (TypeElement) varELe.getEnclosingElement();
           // 拿到包含 enclosingElement 元素的類的名稱 樣式如 com.wangshijia.www.annotationapplication.Hero
           String key = enclosingElement.getQualifiedName().toString();
           messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
           packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
           fields = maps.get(key);
           if (fields == null) {
               maps.put(key, fields = new ArrayList<>());
           }
           fields.add(varELe);
       }
   }

   /*
    * maps 包含有全部被 @Name 修飾的類
    */
   for (String key : maps.keySet()) {
       List<VariableElement> elementFileds = maps.get(key);
       messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
       messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + elementFileds);

       String className = key.substring(key.lastIndexOf(".") + 1);
       className += SUFFIX;
       // 建立 className 類
       TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
               .addModifiers(Modifier.PUBLIC, Modifier.FINAL);
       // 建立方法
       MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("printNameAnnotation")
               .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
               .returns(void.class);
       //建立方法中的打印語句
       for (VariableElement e : elementFileds) {
           Name annotation = e.getAnnotation(Name.class);
           // 建立 printNameAnnotation 方法
           methodBuild
                   .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.name())
                   .addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.alias());

       }

       //將方法中添加到類中
       MethodSpec printNameMethodSpec = methodBuild.build();
       TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();

       try {
           //構造的 java 文件 參數一 包名,參數二 上述構建的類描述 TypeSpec
           JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
                   .addFileComment(" This codes are generated automatically. Do not modify!")
                   .build();
           javaFile.writeTo(filer);
       } catch (IOException exception) {
           exception.printStackTrace();
       }
   }

複製代碼

上述註釋寫的很詳細了,這裏但願不熟悉的朋友,本身動手實現下,才能更好的理解是如何構建對應的文件的。生成的文件位於指定目錄下:

 

WX20180513-122856@2x.png

 

  1. 使用咱們定義好的註解生成文件

    使用註解生成器生成的 java 文件和普通的類沒什麼區別,經過編譯後就放在上述文件夾中,咱們能夠正常調用咱們構造類的方法,ButterKnife.bind(this) 實際上就是調用生成類的方法的過程。咱們是一個簡單的 demo 就不這麼複雜的調用了。直接在 App 目錄下的任意文件調用,如在一個 Activity 中:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        PAHeroAutoGenerate.printNameAnnotation();
        SBHeroAutoGenerate.printNameAnnotation();
    }
}
複製代碼

對於 JavaPoet 生成 java 文件的過程若是想深刻了解的話能夠查看該博客:JavaPoet - 優雅地生成代碼

Android 預置的註解

平常開發中,註解可以幫助咱們寫出更好更優秀的代碼,爲了更好地支持 Android 開發,在已有的 android.annotation 基礎上,Google 開發了 android.support.annotation 擴展包,共計50個註解,幫助開發者們寫出更優秀的程序,這五十多種註解得以應用場景各不相同,常見的如 @IntDef @ColorInt @Nullable。

對於這些註解的用途這裏再也不詳細說明,感興趣的能夠去查看下一個朋友寫的關於 Android 中註解的做用的文章: Android 註解指南

總結

這篇文章寫的時候遇到不少的困難,由於本人對於註解以前瞭解狀況和大多數人同樣,只停留在不多的使用階段,在文章的構成方面也是一改再改。可是功夫不負有心人,在查閱了大量的資料後,學習到了不少註解的使用和原理的知識。也發現本身的知識掌握程度已經落下很多,好比鴻洋大神寫的 Android 打造編譯時註解解析框架 這只是一個開始 這篇文章在15年的時候就有了,想一想當時剛畢業,與大神的距離整整拉開了進3年,讓我去哭一會。可是我的認爲這是件好事。總比一直停留在用上好一些,每次深一步瞭解,就感受我跟大神之間的差距少了一些。

參考

相關文章
相關標籤/搜索