不積跬步無以至千里,不積小流無以成江海。厚積才能薄發,水到自然渠成;
一如既往先提三個問題:註解是什麼?註解怎麼用?註解有什麼作用?
註解是什麼?
註解就是對程序代碼的補充說明,可以理解爲標籤,是對這段程序代碼的解釋,主要的目的就是提高我們的代碼質量,和工作效率
註解怎麼用?
1. 註解的語法
同 class
和 interface
一樣,註解也是一種類型,通過@interface
關鍵字定義,其語法如下:
public @interface TestAnnotation {
}
其定義形式類似於接口,只不過在前面多了一個 @
符號,這裏表示創建了一個TestAnnotation
的註解。
2. 註解的屬性
註解的屬性也叫成員變量,註解只有成員變量,沒有成員方法。 註解的成員變量在註解中以無形參的方法
來聲明,其方法名定義了該成員變量的名字,返回類型定義了該成員變量的類型,比如:
@Inherited//該註解可以它註解的類的子類繼承
@Retention(RetentionPolicy.RUNTIME)//該註解的生命週期
@Target(ElementType.TYPE)
public @interface Test {
int id() default -1;
String msg() default "";
}
上述代碼定義了一個叫 Test
的註解,它擁有 id
和msg
這兩個成員屬性,在使用時,通過在註解的()
中以value= xxx
的形式賦值,多個屬性通過,
隔開,如下所示:
@Test(id = 2, msg = "hello annotation")
public class SuperMan {
@Check("hi")
int a;
@Perform
@Person(role = "PM")
public void testMethod() {
}
@SuppressWarnings("deprecation")
public void test() {
}
}
Tips:
- 註解中只能用public或默認(default)這兩個訪問權修飾;
- 成員屬性只能是八大基本類型
byte,short,char,int,long,float,double,boolean
和String,Enum,Class,annotations
等數據類型,以及這一些類型的數組; - 如果只有一個成員屬性,建議使用
value()
替代 - 註解屬性必須有確認的值,可以在定義註解時使用
default
指定,或者在使用註解時指定,非基本類型的註解元素的值不可爲null,常使用空字符串或負數作爲默認值。
/**
*自定義註解MyAnnotation
*/
@Target(ElementType.TYPE) //目標對象是類型
@Retention(RetentionPolicy.RUNTIME) //保存至運行時
@Documented //生成javadoc文檔時,該註解內容一起生成文檔
@Inherited //該註解被子類繼承
public @interface MyAnnotation {
public String value() default ""; //當只有一個元素時,建議元素名定義爲value(),這樣使用時賦值可以省略"value="
String name() default "devin"; //String
int age() default 18; //int
boolean isStudent() default true; //boolean
String[] alias(); //數組
enum Color {GREEN, BLUE, RED,} //枚舉類型
Color favoriteColor() default Color.GREEN; //枚舉值
}
3. 註解的應用
註解的分類
- 根據成員個數分類
1. 標記註解
沒有定義成員的註解類型,自身就代表了某類信息,比如@override
2. 單成員註解
只定義了一個成員,比如@SuppressWarnings
定義了一個成員String[] value
,使用value={...}
大括號來聲明數組值,一般也可以省略“value=”
3. 多成員註解
定義了多個成員,使用時以name=value對分別提供數據 - 根據用途和功能分類
1. 元註解
元註解,可以理解爲註解的註解,即解釋註解信息的一些特殊標籤,它們主要的功能是確定註解的生命週期@Retention
,範圍@Target
,是否被子類繼承@Inherited
,是否可重複@Repeatable
,是否生成到javac文檔中@Documented
- @Retention
Retention 的英文意爲保留期的意思。當 @Retention 應用到一個註解上的時候,它解釋說明了這個註解的的存活時間。
它的取值如下:
- RetentionPolicy.SOURCE 註解只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。
- RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被加載到 JVM 中。
- RetentionPolicy.RUNTIME 註解可以保留到程序運行的時候,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。
@Retention 相當於給一個註解蓋了一張時間戳,時間戳指明瞭註解的時間週期。
- @Target
Target 是目標的意思,@Target 指定了註解運用的地方。當一個註解被 @Target 註解時,這個註解就被限定了運用的場景。
類比到標籤,原本標籤是你想張貼到哪個地方就到哪個地方,但是因爲 @Target 的存在,它張貼的地方就非常具體了,比如只能張貼到方法上、類上、方法參數上等等。@Target 有下面的取值
- ElementType.ANNOTATION_TYPE 可以給一個註解進行註解
- ElementType.CONSTRUCTOR 可以給構造方法進行註解
- ElementType.FIELD 可以給屬性進行註解
- ElementType.LOCAL_VARIABLE 可以給局部變量進行註解
- ElementType.METHOD 可以給方法進行註解
- ElementType.PACKAGE 可以給一個包進行註解
- ElementType.PARAMETER 可以給一個方法內的參數進行註解
- ElementType.TYPE 可以給一個類型進行註解,比如類、接口、枚舉
- @Inherited
Inherited 是繼承的意思,但是它並不是說註解本身可以繼承,而是說如果一個超類被 @Inherited 註解過的註解進行註解的話,那麼如果它的子類沒有被任何註解應用的話,那麼這個子類就繼承了超類的註解。 比如:
@Inherited//該註解可以它註解的類的子類繼承
@Retention(RetentionPolicy.RUNTIME)//該註解的生命週期
@Target(ElementType.TYPE)
public @interface Test {
int id();
String msg();
}
@Person(role = "產品經理")
@Person(role = "PM")
@Test(id = 2, msg = "hello annotation")
public class SuperMan {
@Check("hi")
int a;
@Perform
@Person(role = "PM")
public void testMethod() {
}
@SuppressWarnings("deprecation")
public void test() {
}
}
public class Man extends SuperMan {
}
public class TestClass {
public static void main(String[] args) {
//首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個註解
boolean hasAnnotation = SuperMan.class.isAnnotationPresent(Test.class);
if (hasAnnotation) {
//返回指定類型的註解
Test annotation = SuperMan.class.getAnnotation(Test.class);
System.out.println("id:" + annotation.id());
System.out.println("msg:" + annotation.msg());
//返回註解到這個元素上的所有註解。
Annotation[] annotations = SuperMan.class.getAnnotations();
for (Annotation an : annotations) {
System.out.println("class SuperMan annotation:" + an.annotationType().getSimpleName());
}
}
//首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個註解
boolean manHasAnnotation = Man.class.isAnnotationPresent(Test.class);
if (manHasAnnotation) {
//返回指定類型的註解
Test annotation = Man.class.getAnnotation(Test.class);
System.out.println("Man id:" + annotation.id());
System.out.println("Man msg:" + annotation.msg());
//返回註解到這個元素上的所有註解。
Annotation[] annotations = Man.class.getAnnotations();
for (Annotation an : annotations) {
System.out.println("class Man annotation:" + an.annotationType().getSimpleName());
}
}
}
結果:
註解 Test
被 @Inherited
修飾,之後類 SuperMan
被 @Test
註解,類 man
繼承 SuperMan
,類 man
也擁有 @Test
這個註解。
- @Repeatable
Repeatable 自然是可重複的意思。@Repeatable 是 Java 1.8 才加進來的,所以算是一個新的特性。(注:只有在java 1.8 中才能如下使用)- 什麼樣的註解會多次應用呢?通常是註解的值可以同時取多個。
- 舉個例子,一個人他既是程序員又是產品經理,同時他還是個畫家。
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {
Person[] value();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Persons.class)
public @interface Person {
String role() default "";
}
@Person(role = "產品經理")
@Person(role = "PM")
@Test(id = 2, msg = "hello annotation")
public class SuperMan {
@Check("hi")
int a;
@Perform
@Person(role = "PM")
public void testMethod() {
}
@SuppressWarnings("deprecation")
public void test() {
}
}
注意上面的代碼,@Repeatable
註解了 Person
。而 @Repeatable
後面括號中的類相當於一個容器註解。
什麼是容器註解呢?就是用來存放其它註解的地方。它本身也是一個註解。
我們再看看代碼中的相關容器註解。
@Retention(RetentionPolicy.RUNTIME)
public @interface Persons {
Person[] value();
}
按照規定,它裏面必須要有一個 value
的屬性,屬性類型是一個被 @Repeatable
註解過的註解數組**,注意它是數組。編譯環境爲 jdk 1.8,低於1.8 會報錯
如果不好理解的話,可以這樣理解。Persons
是一張總的標籤,上面貼滿了 Person
這種同類型但內容不一樣的標籤。把 Persons
給一個 SuperMan
貼上,相當於同時給他貼了程序員、產品經理、畫家的標籤。
- @Documented
它的作用是在生成javadoc文檔的時候將該Annotation也寫入到文檔中
2. 內置註解
@Override
限定重寫父類方法@Deprecated
用於標記已過時@SuppressWarnnings
抑制編譯器警告
3. 自定義註解
使用@interface
自定義註解,在定義註解時,不能繼承其他的註解或接口。比如:
/**
*自定義註解MyAnnotation
*/
@Target(ElementType.TYPE) //目標對象是類型
@Retention(RetentionPolicy.RUNTIME) //保存至運行時
@Documented //生成javadoc文檔時,該註解內容一起生成文檔
@Inherited //該註解被子類繼承
public @interface MyAnnotation {
public String value() default ""; //當只有一個元素時,建議元素名定義爲value(),這樣使用時賦值可以省略"value="
String name() default "devin"; //String
int age() default 18; //int
boolean isStudent() default true; //boolean
String[] alias(); //數組
enum Color {GREEN, BLUE, RED,} //枚舉類型
Color favoriteColor() default Color.GREEN; //枚舉值
}
註解的獲取
我們主要通過反射來獲取運行時註解,常用API 如下:
<T extends Annotation> T getAnnotation(Class<T> annotationClass) :返回該程序元素上存在的、指定類型的註解,如果該類型註解不存在,則返回null;
Annotation[] getDeclaredAnnotation(Class<T>):返回該程序元素上存在的、指定類型的註解,如果該類型註解不存在,則返回null;與此接口中的其他方法不同,該方法將忽略繼承的註解;
Annotation[] getAnnotations():返回該程序元素上存在的所有註解;
Annotation[] getDeclaredAnnotations():返回直接存在於此元素上的所有註釋。與此接口中的其他方法不同,該方法將忽略繼承的註解;
Annotation[] getAnnotationsByType(Class<T>):返回直接存在於此元素上指定註解類型的所有註解;
Annotation[] getDeclaredAnnotationsByType(Class<T>):返回直接存在於此元素上指定註解類型的所有註解。與此接口中的其他方法不同,該方法將忽略繼承的註解;
boolean isAnnotationPresent(Class<?extends Annotation> annotationClass):判斷該程序元素上是否包含指定類型的註解,存在則返回true,否則返回false;
例如:
public class TestClass {
public static void main(String[] args) {
//首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個註解
boolean hasAnnotation = SuperMan.class.isAnnotationPresent(Test.class);
if (hasAnnotation) {
//返回指定類型的註解
Test annotation = SuperMan.class.getAnnotation(Test.class);
System.out.println("id:" + annotation.id());
System.out.println("msg:" + annotation.msg());
//返回註解到這個元素上的所有註解。
Annotation[] annotations = SuperMan.class.getAnnotations();
for (Annotation an : annotations) {
System.out.println("class SuperMan annotation:" + an.annotationType().getSimpleName());
}
}
//首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個註解
boolean manHasAnnotation = Man.class.isAnnotationPresent(Test.class);
if (manHasAnnotation) {
//返回指定類型的註解
Test annotation = Man.class.getAnnotation(Test.class);
System.out.println("Man id:" + annotation.id());
System.out.println("Man msg:" + annotation.msg());
//返回註解到這個元素上的所有註解。
Annotation[] annotations = Man.class.getAnnotations();
for (Annotation an : annotations) {
System.out.println("class Man annotation:" + an.annotationType().getSimpleName());
}
}
try {
Field a = SuperMan.class.getDeclaredField("a");
a.setAccessible(true);
//獲取一個成員變量上的註解
Check check = a.getAnnotation(Check.class);
if (null != check) {
System.out.println("check value:" + check.value());
}
Method testMethod = SuperMan.class.getDeclaredMethod("testMethod");
if (null != testMethod) {
Annotation[] ans = testMethod.getAnnotations();
for (Annotation an : ans) {
System.out.println("method testMethod annotation:" + an.annotationType().getSimpleName());
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
運行結果:
註解的使用
栗子:
在Android
中的 findViewById
可以用註解這樣寫:
@BindView(R.id.hello_tv)
TextView helloTv;
點擊事件可以這樣寫:
@OnClick({R.id.toast_btn, R.id.change_btn})
public void onClick(View view) {
switch (view.getId()) {
case R.id.toast_btn:
Toast.makeText(MainActivity.this, "click toast button", Toast.LENGTH_SHORT).show();
break;
case R.id.change_btn:
helloTv.setText(getResources().getString(R.string.changebtn));
break;
default:
Toast.makeText(MainActivity.this, "default is called ", Toast.LENGTH_SHORT).show();
break;
}
}
BindView
這裏我們定義了一個 @BindView
註解,因爲是使用在屬性上的,所以聲明Targe
註解爲 Field
,又因爲是在程序運行中使用,故Retention
指定爲 RUNTIME
,這裏我們需要傳入了控件id
,所以需要再定義一個int
類型的屬性接收,完整代碼如下所示:
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
int value();
}
OnClick
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
int[] value();
//setOnClickListener 爲點擊事件的set 方法默認名
String listenerSetter() default "setOnClickListener";
//OnClickListener 點擊事件監聽的類型
Class listenerType() default View.OnClickListener.class;
//點擊事件的默認方法名
String methodName() default "onClick";
}
ViewHandler
點擊事件動態代理類
public class ViewHandler implements InvocationHandler {
private Object mObject;
private String mMethodName;
private Method mMethod;
public ViewHandler(Object object, String methodName, Method method) {
mObject = object;
this.mMethodName = methodName;
this.mMethod = method;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals(mMethodName)) {
return mMethod.invoke(mObject, args);
}
return null;
}
}
現在我們已經可以拿到控件的id
和註解,但此時我們還不能直接使用這個註解,因爲它和我們的控件還沒什麼關聯,現在我們通過反射來實現註解的注入。如下:
public class AnnoUtils {
private static final String TAG = "AnnoUtils";
public static void inject(Activity activity) {
if (null == activity) {
return;
}
try {
//獲取注入的 activity 的Class 對象
Class clazz = activity.getClass();
//獲取該 Aty 的所有屬性
Field[] fields = clazz.getDeclaredFields();
if (null == fields || fields.length <= 0) {
return;
}
//遍歷所有屬性
for (Field field : fields) {
//判斷當前字段是否支持 BindView 註解
if (field.isAnnotationPresent(BindView.class)) {
//獲取註解對象
BindView annotation = field.getAnnotation(BindView.class);
if (null != annotation) {
//獲取註解內容
int id = annotation.value();
View view = activity.findViewById(id);
if (view != null) {
//把view賦給字段filed
field.setAccessible(true);
field.set(activity, view);
}
}
}
}
Method[] methods = clazz.getDeclaredMethods();
if (null == methods || methods.length <= 0) {
return;
}
for (Method method : methods) {
if (method.isAnnotationPresent(OnClick.class)) {
OnClick annotation = method.getAnnotation(OnClick.class);
if (annotation != null) {
//獲取所有的id
int[] ids = annotation.value();
for (int id : ids) {
//找到相應的view
View view = activity.findViewById(id);
if (view != null) {
method.setAccessible(true);
String listenerSetter = annotation.listenerSetter();
Class listenerType = annotation.listenerType();
String methodName = annotation.methodName();
ViewHandler viewHandler = new ViewHandler(activity, methodName, method);
//獲取onClickListener代理對象
Object onClickListener = Proxy.newProxyInstance(listenerType.getClassLoader(), new Class[]{listenerType}, viewHandler);
//獲取view的setOnClickListener方法
Method setOnListenerMethod = view.getClass().getMethod(listenerSetter, listenerType);
//實現setOnClickListener方法
setOnListenerMethod.invoke(view,onClickListener);
}
}
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
具體意思代碼中都有註釋,就不多說了,接着看下我們的Activity
代碼
public class MainActivity extends AppCompatActivity {
@BindView(R.id.hello_tv)
TextView helloTv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnnoUtils.inject(this);
helloTv.setText(getResources().getString(R.string.change_by_annotation));
}
@OnClick({R.id.toast_btn, R.id.change_btn})
public void onClick(View view) {
switch (view.getId()) {
case R.id.toast_btn:
Toast.makeText(MainActivity.this, "click toast button", Toast.LENGTH_SHORT).show();
break;
case R.id.change_btn:
helloTv.setText(getResources().getString(R.string.changebtn));
break;
default:
Toast.makeText(MainActivity.this, "default is called ", Toast.LENGTH_SHORT).show();
break;
}
}
}
XML 代碼:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/hello_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/change_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/change"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/hello_tv" />
<Button
android:id="@+id/toast_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/toast"
android:textAllCaps="false"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/change_btn" />
</androidx.constraintlayout.widget.ConstraintLayout>
效果圖如下:
GitHub demo鏈接
註解有什麼作用?
- 編寫文檔:通過代碼裏標識的元數據生成文檔。
- 代碼分析:通過代碼裏標識的元數據對代碼進行分析。
- 編譯檢查:通過代碼裏標識的元數據讓編譯器能實現基本的編譯檢查。
- 通過不同註解明規範功能和一些統一的操作,比如retrofit、Junit、dagger2
個人思考:
註解它的主要功能就是提供它承載的信息。其實我們在項目開發中,主要有幾個指標,一是開發效率要高,二是代碼質量要高,三是開發完項目的維護成本要低,其實註解就能很好的承擔這些角色,比如提高開發效率的Rertofit
、Butterknife
這些框架中的註解,提高代碼質量的有Junit
,降低維護成本的比如內置註解@Deprecated
、@Override
這些,他們都是爲了幫助我們理解代碼,分析代碼,統一規範行爲,提高代碼質量,因爲一個無法避免的問題就是隨着項目的迭代,項目的可讀性、可靠性會下降,它的維護開發成本會提升,註解是解決這些問題的一個很好的方式,但是註解的提取需要藉助於 Java 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。
參考文章:
https://blog.csdn.net/vv_bug/article/details/64500453
https://blog.csdn.net/briblue/article/details/73824058