Java 夯實基礎之註解

不積跬步無以至千里,不積小流無以成江海。厚積才能薄發,水到自然渠成;

一如既往先提三個問題:註解是什麼?註解怎麼用?註解有什麼作用?

註解是什麼?

  註解就是對程序代碼的補充說明,可以理解爲標籤,是對這段程序代碼的解釋,主要的目的就是提高我們的代碼質量,和工作效率

註解怎麼用?

1. 註解的語法

   同 classinterface 一樣,註解也是一種類型,通過@interface 關鍵字定義,其語法如下:

public @interface TestAnnotation {

}

其定義形式類似於接口,只不過在前面多了一個 @ 符號,這裏表示創建了一個TestAnnotation 的註解。

2. 註解的屬性

   註解的屬性也叫成員變量,註解只有成員變量,沒有成員方法。 註解的成員變量在註解中以無形參的方法來聲明,其方法名定義了該成員變量的名字,返回類型定義了該成員變量的類型,比如:

@Inherited//該註解可以它註解的類的子類繼承
@Retention(RetentionPolicy.RUNTIME)//該註解的生命週期
@Target(ElementType.TYPE)
public @interface Test {
    int id() default -1;
    String msg() default "";
}

上述代碼定義了一個叫 Test 的註解,它擁有 idmsg這兩個成員屬性,在使用時,通過在註解的()中以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,booleanString,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
    個人思考:
      註解它的主要功能就是提供它承載的信息。其實我們在項目開發中,主要有幾個指標,一是開發效率要高,二是代碼質量要高,三是開發完項目的維護成本要低,其實註解就能很好的承擔這些角色,比如提高開發效率的RertofitButterknife 這些框架中的註解,提高代碼質量的有Junit,降低維護成本的比如內置註解@Deprecated@Override這些,他們都是爲了幫助我們理解代碼,分析代碼,統一規範行爲,提高代碼質量,因爲一個無法避免的問題就是隨着項目的迭代,項目的可讀性、可靠性會下降,它的維護開發成本會提升,註解是解決這些問題的一個很好的方式,但是註解的提取需要藉助於 Java 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。

參考文章:
https://blog.csdn.net/vv_bug/article/details/64500453
https://blog.csdn.net/briblue/article/details/73824058

發佈了38 篇原創文章 · 獲贊 5 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章