夯實 Java 基礎 - 注解
不知道大家有沒有一種感覺,當你想要了解某個知識點的時候,就會發現好多技術類 APP 或者公眾號在推一些關于這個知識點的文章。也許這就是大數據的作用,這也說明總有人比你搶先一步。學習不能停滯,要不你就會被別人越落越遠。
本文接著來回顧和總結 Java 基礎中注解的知識點和簡單的使用,同樣本文將從以下幾個方面來回顧注解知識:
- 注解的定義
- 注解的語法
- 源碼級別的注解的使用
- 運行時注解的使用
- 編譯時注解的使用
- Android 預置的注解
注解的定義
注解(Annotation),也叫元數據。一種代碼級別的說明。它是 JDK 1.5 以后版本引入的一個特性,與類、接口、枚舉是在同一個層次。它可以聲明在包、類、字段、方法、局部變量、方法參數等元素上。它提供數據用來解釋程序代碼,但是注解并非是所解釋的代碼本身的一部分。注解對于代碼的運行效果沒有直接影響。
注解有許多用處,主要如下:
- 提供信息給編譯器: 編譯器可以利用注解來探測錯誤和警告信息
- 編譯階段時的處理: 軟件工具可以用來利用注解信息來生成代碼、Html 文檔或者做其它相應處理。
- 運行時的處理: 某些注解可以在程序運行的時候接受代碼的提取
如我們所熟知的依賴注入框架 ButterKnife
就是在編譯階段來生成 findViewById
的代碼(文件)的,而我們所見過的 @Deprecated
就是提供信息給編輯器的RetentionPolicy.SOURCE
類型注解,說明這個屬性已經過時的,對于運行時的注解在反射的文章的最后我們也舉了個小例子,說明了它的作用。
在自定義了一個編譯或者運行階段的注解后,需要一個開發者編寫相應的代碼來解釋這些注解,從而來發揮注解的作用。這些用來解釋注解的代碼被統稱為是 APT(Annotation Processing Tool)
。換句話說注解其實是給 APT 或者編輯器來使用的,而對于非框架開發人員的我們我們只需要關注注解的使用,并遵守規則即可,從而我們節省了很多代碼提高了效率。
但是凡事如果只滿足于用上,就不算是一個合 (tong) 格 (guo) 程 (mian)序 (shi) 員 (de)! 但是不要慌,當你打開這篇文章的時候你已經離 offer 又進了一步。
注解的語法
注解的聲明
注解的聲明和聲明一個接口十分類似,沒錯只是名字很類似~ 我們使用@interface
來聲明一個注解,如我們最常見的Override
注解的聲明
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
注解聲明的修飾符,可以是 private
,public
, protected
或者默認 default
這一點跟定義一個類或者接口相同。
在聲明一個注解的時候我們常常需要一些其他注解來修飾和限制該定義注解的使用和運行方式。上述的 @Target
和 @Retention
就是如此,我們稱之為元注解,詳細的元注解在下邊說明。
注解成員
注解跟一個類相似,它們并不是都是像上面的 @Override
一樣只有聲明。一個類大概可以包含構造函數,成員變量,成員函數等,而一個注解只能包含注解成員,注解成員的聲明格式為:
類型 參數名() default 默認值;
注解成員可以是:
基本類型
byte
,short
,int
,long
,float
,double
,boolean
八種基本類型及這些類型的數組, 注意這里沒對應基本數據類型的包裝類。String
,Enum
,Class
,annotations
及這些類型的數組注解的成員修飾符只能是
public
或默認(default)注解元素必須有確定的值,可以在注解中定義默認值,也可以使用注解時指定。即我們在定義注解的時候聲明的成員,可以不賦值,但是就跟抽象函數一樣,在使用的時候就必須指定。
如:
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
類中,包括:
ElementType.ANNOTATION_TYPE
可以給一個注解進行注解ElementType.CONSTRUCTOR
可以給構造方法進行注解ElementType.FIELD
可以給屬性進行注解ElementType.LOCAL_VARIABLE
可以給局部變量進行注解ElementType.METHOD
可以給方法進行注解ElementType.PACKAGE
可以給一個包進行注解ElementType.PARAMETER
可以給一個方法內的參數進行注解ElementType.TYPE
可以給一個類型進行注解,比如類、接口、枚舉
其中 METHOD
、PARAMETER
、FIELD
最為常見,如 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
經過運行時注解的了解,相比對于注解應該都有一個大概的了解了。接下來到了編譯時注解,這個注解類型,便是眾多工具庫中應用的注解類型,它不會影響運行時的效率問題,而是在編譯期,或者打包過程中就生成了對應的代碼,在運行時將會生效。如我們常見的 ButterKnife
和 EventBus
。
編譯時注解與運行時注解不同,編譯時注解主要是幫助我們在編譯器編譯期使用注解處理器生成相應的代碼,幫我們解放勞動力。
我們知道運行時注解是通過反射來解釋對應注解并使注解生效的,那么編譯時如何解釋對應的注解呢?這里就需要用到注解處理器的知識了。
注解處理器(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) { }
}
注解處理器的處理步驟主要有以下:
- 編譯器開始執行注解處理器
- process 方法中循環處理注解元素(Element),找到被該注解所修飾的類,方法,或者屬性
- 拿上一步得到的備注注解修飾的類或者屬性,方法,生成對應的輔助類,并寫入 java 文件
- 生成 java 文件后就可以在運行時,在程序中獲取并調用對應的輔助方法,如
ButterKnife.bind(this);
方法就是獲取對應 Activity 的注解處理器生成的java 文件,并執行了構造函數。
自定義一個編譯時注解
自定義編譯時注解要比運行時注解要繁瑣一些。下面我們來舉一個簡單的例子,意在說明編譯時注解是如何工作的。
在 Android 中為了實現一個編譯時注解我們一般需要借助兩個三方庫:
com.google.auto.service:auto-service:1.0-rc2
這是谷歌官方提供的一個注解處理注冊插件可以幫助我們更方便的注冊注解處理器,只需要在自定義的 Processor 類上方添加@AutoService(Processor.class)
即可,不用自己動手執行注解處理器的注冊工作(即編寫 resource/META-INF/services/javax.annotation.processing.Processor文件)。為了更方便的在 process 文件中生成 Java 類,需要依賴一個 Square 公司開源的 javapoet 庫,
com.squareup:javapoet:1.9.0
這個庫中包裝提供了一些好用的 API 幫助我們更快更準確的構建 .java 文件。當然你也可以自己手寫拼接字符串然后寫入文件(如果你能保證正確)。
仿照 ButterKnife 的實現,我們建立一個新的 Android project ,然后創建兩個 Java Moudle,其中 processor
用來存放注解處理器,processor-lib
用來存放對應的注解,如下圖所示:
在注解處理器存在的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')
}
好了經過上述的準備我們終于能夠編寫我們的編譯時注解了:
- 在 processor-lib 定義一個 Name 注解如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Name {
String name();
String alias();
}
- 編寫兩個類使用我們定義的注解:
public class SBHero {
@Name(name = "Spirit Walker", alias = "SB")
private String heroName;
}
public class PAHero {
@Name(name = "Phantom Assassin", alias = "PA")
private String heroName;
}
- 在 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;
}
...
}
-
最后我們要編輯我們的 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();
}
}
上述注釋寫的很詳細了,這里希望不熟悉的朋友,自己動手實現下,才能更好的理解是如何構建對應的文件的。生成的文件位于指定目錄下:
-
使用我們定義好的注解生成文件
使用注解生成器生成的 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年,讓我去哭一會。但是個人認為這是件好事。總比一直停留在用上好一些,每次深一步了解,就感覺我跟大神之間的差距少了一些。
參考
- Thinking In java
- Android 打造編譯時注解解析框架 這只是一個開始
- Android 注解指南
- JavaPoet - 優雅地生成代碼