美文网首页
谈谈你对注解的理解

谈谈你对注解的理解

作者: Marker_Sky | 来源:发表于2020-08-25 14:54 被阅读0次
    目录

    前言

    刚写了一篇有关 CoordinatorLayoutAppBarLayout 的文章,里面有提到过 AppBarLayout 的 Behavior 是通过注解实现的,本文就通过这个过程来简单分析下注解以及使用。

    一、注解简述(Annotation )

    1.1 定义

    什么是注解?

    • Java 1.5 开始引入的一种标注,相当于给代码打一个 tag、作一个标记。
    1.2 作用

    有什么用?

    • 编译期:让编译器 / APT(Annotation Processing Tool)根据注解的定义,去执行一些逻辑;
    • 运行期:让虚拟机根据字节码中的注解,执行一些逻辑。
    1.3 使用

    怎么用?

    1. 定义注解:

    创建一个文件或一段代码,类型为 @interface。这样该 “类” 就变成了一个注解,为什么 “类”
    要加引号呢,因为真正的类是 class 修饰的,而 @interface 修饰的就是一个注解。我们就拿 CoordinatorLayout 中的 DefaultBehavior 举例:

    CoordinatorLayout # DefaultBehavior

    @Deprecated
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DefaultBehavior {
        Class<? extends CoordinatorLayout.Behavior> value();
    }
    

    这时需要提到一个接口:Annotation,定义类型为 @interface 表示该文件或该 “类” 继承于 Annotation 接口,说明该 "类" 定义为了注解。

    Annotation 接口

    package java.lang.annotation;
    // 所有注解类型都会继承于这个接口。
    public interface Annotation {
        // 用来比较两个注解类型是否相同
        boolean equals(Object obj);
        
        int hashCode();
    
        String toString();
        // 获取注解类型
        Class<? extends Annotation> annotationType();
    }
    

    现在 DefaultBehavior 已经创建出来了,我们暂且不管这个注解上面的两个 @,来看下是怎么使用的。

    2. 设置注解,传参:

    google 文档说,如果要指定一个 View 的 Behavior,除了可以在 xml 中使用 app:layout_behavior, 也可以使用注解。就是在这个 View 类顶部注明,比如 AppBarLayout 中的 @DefaultBehavior(AppBarLayout.Behavior.class)

    AppBarLayout

    @DefaultBehavior(AppBarLayout.Behavior.class)
    public class AppBarLayout extends LinearLayout {
        ...
    }
    

    这样就给 AppBarLayout 指定了注解,然而可以注意到,注解的参数里传了一个类 AppBarLayout.Behavior.class。我们再回头看一下注解创建的时候:

    @Deprecated
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DefaultBehavior {
        Class<? extends CoordinatorLayout.Behavior> value();
    }
    

    其内部定义了一个 value() 方法,这个方法的返回值是一个继承了 CoordinatorLayout.Behavior 的 Class。所以在使用注解时传递的值必须为 CoordinatorLayout.Behavior 的子类,而 AppBarLayout.Behavior 就是继承于 AppBarLayout.BaseBehavior 的:

    public static class Behavior extends AppBarLayout.BaseBehavior<AppBarLayout> {...}
    AppBarLayout.BaseBehavior extends... CoordinatorLayout.Behavior
    

    这样就完成了 设置注解并传参 的过程。

    3. 读取注解

    注解已经添加到了 AppBarLayout 上,接下来看一下是如何读取并应用到代码逻辑里的。我们知道之前例子里 AppBarLayout 的 Behavior 是由 CoordinatorLayout 管理的,所以去源码里顺藤摸瓜:

    CoordinatorLayoutonMeasure() 方法会调用一个 prepareChildren() 方法,顾名思义,该方法是用来准备子 View 的:

    CoordinatorLayout # prepareChildren()

    private void prepareChildren() {
        this.mDependencySortedChildren.clear();
        this.mChildDag.clear();
        int i = 0;
    
        for(int count = this.getChildCount(); i < count; ++i) {
            View view = this.getChildAt(i);
            // 读取子 View 的 LayoutParams
            CoordinatorLayout.LayoutParams lp = this.getResolvedLayoutParams(view);
            lp.findAnchorView(this, view);
            this.mChildDag.addNode(view);
    
            for(int j = 0; j < count; ++j) {
                if (j != i) {
                    View other = this.getChildAt(j);
                    if (lp.dependsOn(this, view, other)) {
                        if (!this.mChildDag.contains(other)) {
                            this.mChildDag.addNode(other);
                        }
    
                        this.mChildDag.addEdge(other, view);
                    }
                }
            }
        }
        // 一个 View 集合,维护子 View
        this.mDependencySortedChildren.addAll(this.mChildDag.getSortedList());
        Collections.reverse(this.mDependencySortedChildren);
    }
    

    这个方法大概就是遍历子 View,读取它们设置的 CoordinatorLayout.LayoutParams 并添加到维护的子 View 集合里。在读取的过程中拿根据子 View 的注解设置的 Behavior:

    CoordinatorLayout # getResolvedLayoutParams()

    CoordinatorLayout.LayoutParams getResolvedLayoutParams(View child) {
        CoordinatorLayout.LayoutParams result = (CoordinatorLayout.LayoutParams)child.getLayoutParams();
        if (!result.mBehaviorResolved) {
            if (child instanceof CoordinatorLayout.AttachedBehavior) {
                ...
            } else {
                Class<?> childClass = child.getClass();
    
                CoordinatorLayout.DefaultBehavior defaultBehavior;
                // 遍历找到子 view 设置的 DefaultBehavior
                for(defaultBehavior = null;
                    childClass != null && (defaultBehavior = (CoordinatorLayout.DefaultBehavior)childClass.getAnnotation(CoordinatorLayout.DefaultBehavior.class)) == null;
                    childClass = childClass.getSuperclass()) {
                }
    
                if (defaultBehavior != null) {
                    try {
                        // 创建 DefaultBehavior 并设置给该 View 的 LayoutParams
                        result.setBehavior((CoordinatorLayout.Behavior)defaultBehavior.value().getDeclaredConstructor().newInstance());
                    } catch (Exception var6) {
                        Log.e("CoordinatorLayout", "Default behavior class " + defaultBehavior.value().getName() + " could not be instantiated. Did you forget" + " a default constructor?", var6);
                    }
                }
                result.mBehaviorResolved = true;
            }
        }
        return result;
    }
    

    上面的核心代码流程:

    • 获取类设置的注解
      defaultBehavior = (CoordinatorLayout.DefaultBehavior)childClass.getAnnotation(CoordinatorLayout.DefaultBehavior.class)
      假设这里遍历到了 AppBarLayout,那么通过 getAnnotation() 方法获取到的注解就是 DefaultBehavior
    • 获取注解设置的值
      defaultBehavior.value()
      这句代码的作用就是拿到注解传递的参数,之前我们说到过注解必须包含一个 value() 方法,就是用在了这里。

    之前 AppBarLayout 设置的注解,传递的是 AppBarLayout.Behavior.class

    @DefaultBehavior(AppBarLayout.Behavior.class)
    public class AppBarLayout extends LinearLayout {
        ...
    }
    
    • 创建实例
      有了 class 就可以利用反射创建该类的实例了:
    result.setBehavior((CoordinatorLayout.Behavior)defaultBehavior.value().getDeclaredConstructor().newInstance());
    

    注解的 value() 方法获取到了 Class 类,然后再调用 getDeclaredConstructor() 方法获取到构造器,最后 newInstance() 创建类的实例。

    也就是说上面的一系列代码通过注解获取到 AppBarLayout 设置的 AppBarLayout.Behavior.class,然后通过反射创建 AppBarLayout.Behavior 的实例,最后再通过 CoordinatorLayout.LayoutParamssetBehavior 方法把 Behavior 设置给 AppBarLayout

    到这里 AppBarLayout 通过注解设置的 Behavior 已经设置到了 AppBarLayout 的 CoordinatorLayout.LayoutParams 中了,父 View CoordinatorLayout 就可以根据该属性获取并使用它的 Behavior 了。

    二、更多注解

    上面已经简单介绍了注解使用流程,我们再回来看之前注解没有提到的内容:

    @Deprecated
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DefaultBehavior {
        Class<? extends CoordinatorLayout.Behavior> value();
    }
    

    首先 DefaultBehavior 是一个注解,但是它的上面还存在两个 @,而 @Deprecated@Retention() 也是两个注解。也就是说 DefaultBehavior 这个注解被标记了。专门作用在注解上的注解称之为元注解

    2.1 元注解

    Java 在 1.5 定义了四个元注解 @Retention、@Documented、@Target 和 @Inherited,在 1.8 又新增了两个元注解 @Repeatable @Native

    package java.lang.annotation # Retention

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Retention {
        /**
         * Returns the retention policy.
         * @return the retention policy
         */
        RetentionPolicy value();
    }
    
    • @Retention(保留):传入参数表示这段注解的保留策略,传参 RetentionPolicy 是一个枚举
      • RetentionPolicy.SOURCE:表示只在编译时期生效,过了编译期就不再保存该注解的信息了;
      • RetentionPolicy.CLASS:表示将该注解保留到 class 文件中,默认行为;
      • RetentionPolicy.RUNTIME:保留到 class 文件中,且可以在运行时被 JVM 读取。

    比如上面的例子,@Retention(RetentionPolicy.RUNTIME) 说明 @interface DefaultBehavior 这个注解的信息会保留到 class 文件中,并且可以被 JVM 读取。不设置这样的保留策略的话,上面的 (CoordinatorLayout.DefaultBehavior)childClass.getAnnotation(CoordinatorLayout.DefaultBehavior.class)) 就获取不到该注解了。

    package java.lang.annotation # Documented

    @Documented
    @Retention(RetentionPolicy.RUNTIME) // 保留策略为存放在 class 文件且运行中可读取
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Documented {
    }
    
    • @Documented:标记该注解能出现在 Javadoc 中。Javadoc 是一个文档生成工具,加上该标记,注解类型信息也会被包括在生成的文档中。

    package java.lang.annotation # Target

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Target {
        /**
         * Returns an array of the kinds of elements an annotation type
         * can be applied to.
         * @return an array of the kinds of elements an annotation type
         * can be applied to
         */
        ElementType[] value();
    }
    

    实例:

    @Documented
    @Target({ ElementType.TYPE, ElementType.METHOD })
    public @interface MyDocumented {
        public String value() default "这是@Documented注解";
    }
    

    新建类:

    @MyDocumented
    public class DocumentedTest {
        /**
         * 测试document
         */
        @MyDocumented
        public String Test() {
            return "Documented测试";
        }
    }
    

    打开 java 文件目录,输入 javadoc 命令:

    javac MyDocumented.java DocumentedTest.java
    javadoc -d doc MyDocumented.java DocumentedTest.java
    

    运行成功会生成帮助文档,如下


    文档
    • @Target:用来指定该注解应该标记的那种 Java 类型。传参 ElementType 也是一个枚举:
      • TYPE:类、接口(包括注释类型)或枚举声明
      • FIELD:字段声明(包括枚举常量)
      • METHOD:方法声明
      • PARAMETER:参数声明
      • CONSTRUCTOR:构造方法声明
      • LOCAL_VARIABLE:局部变量声明
      • ANNOTATION_TYPE:注释类型声明
      • PACKAGE:包声明

    实例:
    声明一个注解,指定 Target 为 TYPE (类、接口(包括注释类型)或枚举声明):

    @Target(ElementType.TYPE)
    public @interface TestAnnotation {
        String value();
    }
    

    那么这个注解就可以写在 类、接口或枚举类 的上面:

    @TestAnnotation(value = "")
    public class LoginAspectActivity extends AppCompatActivity {
    }
    

    如果注解在方法上面,编译器会直接报错:

    @TestAnnotation(value = "") // 抛红报错
    public void area(View view) {
    }
    

    如果想让这个注解,既可以在类上使用,也可以在方法使用,可以这样写:

    @Target({ElementType.TYPE,ElementType.METHOD})
    public @interface TestAnnotation {
        String value();
    }
    

    这个这个注解就可以在多种类型的代码上生效了,我们可以根据需要自定义注解的作用类型。

    package java.lang.annotation # Inherited

    @Documented // 可出现在 javadoc 中
    @Retention(RetentionPolicy.RUNTIME) // 保留策略为存放在 class 文件且运行中可读取
    @Target(ElementType.ANNOTATION_TYPE) // 只可以标注注解类型
    public @interface Inherited {
    }
    
    • @Inherited :标记该注解具有继承性。被标记为可继承后,使用该注解的所有子类都可以获取到父类的注解信息。

    实例:

    1. 首先我们准备两个注解,一个 @Inherited 标注,另一个没有。
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    public @interface TestAnnotation {
        String value();
    }
    
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TestAnnotation2 {
        String value();
    }
    
    1. 写一个类同时使用这两个注解。
    @TestAnnotation("该注解具有继承性")
    @TestAnnotation2("该注解不具有继承性")
    public class Father {
    }
    
    1. 再准备一个子类,继承于 Father 类。
    public class Child extends Father {
    }
    
    1. 进行测试:
    public static void main(String[] args){
        Class<Child> childClass = Child.class;
        // 是否持有该注解
        if (childClass.isAnnotationPresent(TestAnnotation.class)) {
            System.out.println(childClass.getAnnotation(TestAnnotation.class).value());
        }
        System.out.println("============");
        if (childClass.isAnnotationPresent(TestAnnotation2.class)) {
            System.out.println(childClass.getAnnotation(TestAnnotation2.class).value());
        }
    }
    

    逻辑很简单,首先检查 Child 类是否持有注解,如果有就打印注解传参。打印 Log:

    该注解具有继承性
    ============
    

    根据结果可知,Child 类可以获取到的注解是 TestAnnotation,也就获取到了被 @Inherited 标注的注解。并且能通过 value() 方法获取到注解传递的参数值。

    package java.lang.annotation # Repeatable

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.ANNOTATION_TYPE)
    public @interface Repeatable {
        /**
         * Indicates the <em>containing annotation type</em> for the
         * repeatable annotation type.
         * @return the containing annotation type
         */
        Class<? extends Annotation> value();
    }
    
    • @Repeatable:Java 8 新增注解,允许重复注解。

    举例

    // 要支持重复注解的容器
    public @interface Study{
        Student[] value();
    }
    // 声明支持重复注解,传入容器信息
    @Repeatable(Study.class)
    public @interface Student {
        String study();
    }
    public class Test {
        @Student(study = "math")
        @Student(study = "english")
        public String doString(){
            return "";
        }
    }
    
    • @Native

    使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。

    2.2 编译期注解

    接下来看另外三个常用的、Java 自带的注解:@Deprecated @Override @SuppressWarnings

    package java.lang # Deprecated

    @Documented // javadoc 可收集信息
    @Retention(RetentionPolicy.RUNTIME) // 保留策略为存放在 class 文件且运行中可读取
    // 可以标注 构造器、字段、局部变量、方法、包、参数、类和接口
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) 
    public @interface Deprecated {
    }
    
    • @Deprecated: 所标注的内容,不再推荐使用。
      比较常见的注解,前文就出现过:

    CoordinatorLayout # DefaultBehavior

    @Deprecated // 已废弃,不推荐使用
    @Retention(RetentionPolicy.RUNTIME) // 保留策略为存放在 class 文件且运行中可读取
    public @interface DefaultBehavior {
        Class<? extends CoordinatorLayout.Behavior> value();
    }
    

    该注解被标记为已废弃,如果继续使用编译器会抛红提示。


    已废弃

    当然,不推荐使用并不代表现在不能用。但还是不推荐使用,说不定某个版本被注解为已废弃的类或者代码会被删除,会给开发造成一定的麻烦。

    package java.lang # Override

    @Target(ElementType.METHOD) // 只能在方法上使用
    @Retention(RetentionPolicy.SOURCE) // 保留策略:只在编译期生效
    public @interface Override {
    }
    
    • @Override: 只能标注方法,表示该方法覆盖父类的方法。
      当父类存在同名方法,加上 @Override 表示覆盖父类的方法。如果不存在,强行在方法上面添加 @Override,编译器会报错。
      可以看到该注解的保留策略为 RetentionPolicy.SOURCE,也就是说该注解的作用仅限于编译器。等到过了编译期,就不再有效了。

    package java.lang # SuppressWarnings

    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
    // 能够标注 类和接口、字段、方法、参数、构造器、局部变量
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value();
    }
    
    • @SuppressWarnings: 表示忽略编译器的警告。

    比如我们有一个标注了废弃的类:

    @Deprecated
    public class DetailActivity extends BaseAppCompatActivity {
    }
    

    在某个 Activity 想要跳转到这个类,正常使用的话编译器会标红提示:

    使用已废弃代码

    这时可以给这个方法添加 @SuppressWarnings 注解并传入要忽略的类型忽略警告:

    忽略警告
    可以看到编译器不再划红线提示了,我们可以根据传入一个或多个类型来决定忽略的警告种类。不过开发过程中,遇到警告最好是去解决而不是忽略,养成好的编码习惯没有坏处。

    到这里,Java 自带的一些注解已经记录完毕,简单总结下:

    名称 作用 保留策略
    @Retention 指定保留策略 运行期
    @Documented javadoc 工具生成文档 运行期
    @Target 指定注解可标记类型 运行期
    @Inherited 指定注解具有继承性 运行期
    @Repeatable 指定注解可重复使用 运行期
    @Deprecated 所标注的内容,不再推荐使用 运行期
    @Override 表示该方法覆盖父类方法 编译期
    @SuppressWarnings 忽略指定警告 编译期

    三、注解的使用

    文章开头根据我自己的理解,写到注解的作用主要有两个:

    • 编译期:让编译器根据注解的定义,去执行一些逻辑;
    • 运行期:让虚拟机根据字节码中的注解,执行一些逻辑。
    3.1 编译期

    在编译期的工作主要是由编译器来完成的,有关编译器的工作原理暂不深究。我们只要知道加上一些编译期的注解,编译器工作时会告知开发者就行。

    比如上面的 @Override,当我们使用 @Override 注解,编译器会自动去父类寻找可覆盖的方法。如果能找到则没问题,找不到编译器就会抛红提示,无法成功编译。

    3.2 运行期

    运行期的作用也比较好理解,首先需要该注解的保留策略为 RetentionPolicy.SOURCE,保留到 class 文件且可被 JVM 读取。
    然后在运行的时候就可以获取到 class 的注解以及设置的参数,再利用反射结合参数达到创建、修改对象信息的作用。接下来写一个简单的例子:

    1. 定义注解

    定义一个注解,指定注解要保存、传递的数据类型:

    // 指定可标记类型为 类/接口/枚举、字段。方法
    @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
    // 指定该注解信息会保留到 class 文件,且在运行时可被 JVM 读取
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotation {
        // 可传递 int 类型
        int type();
    
        // 可传递 String、有默认值
        String info() default "Hello";
    }
    

    当注解只包含一个返回时,可以写成 value() 方法,这样传递参数的时候不需要特别指定。

    2. 使用注解

    创建一个类,分别在 类、变量、方法 使用注解

    // 注解作用在类
    @MyAnnotation(type = 0)
    public class Game {
    
        // 作用在变量
        @MyAnnotation(type = 1, info = "int")
        private int i;
    
        // 作用在方法
        @MyAnnotation(type = 2, info = "startGame")
        @Deprecated
        private void start(String s) {
            System.out.println("游戏开始:" + s);
        }
    }
    
    3. 获取注解

    首先判断类是否持有注解,然后利用 反射 获取注解信息。

    // 获取类
    Class<Game> gameClass = Game.class;
    // 判断是否持有 MyAnnotation 注解
    if (gameClass.isAnnotationPresent(MyAnnotation.class)) {
        // 获取注解
        MyAnnotation classAnnotation = gameClass.getAnnotation(MyAnnotation.class);
        if (null != classAnnotation) {
            int type = classAnnotation.type();
            String info = classAnnotation.info();
            System.out.println("类注解中的信息,type:" + type + "----info:" + info);
            // 打印结果:        类注解中的信息,type:0----info:Hello
        }
        try {
            // 通过反射获取变量 参数:变量名
            Field field = gameClass.getDeclaredField("i");
            MyAnnotation fieldAnnotation = field.getAnnotation(MyAnnotation.class);
            if (null != fieldAnnotation) {
                int type = fieldAnnotation.type();
                String info = fieldAnnotation.info();
                System.out.println("变量注解中的信息,type:" + type + "----info:" + info);
                // 打印结果:       变量注解中的信息,type:1----info:int
            }
            // 获取方法 第一个参数:方法名,第二个:传参类型
            Method method = gameClass.getDeclaredMethod("start", String.class);
            // 获取持有的所有注解
            Annotation[] annotations = method.getAnnotations();
            for (Annotation annotation : annotations) {
                System.out.println("方法注解中的信息" + annotation.annotationType().getSimpleName());
                // 打印结果:       方法注解中的信息MyAnnotation
                // 打印结果:       方法注解中的信息Deprecated
                if(annotation instanceof MyAnnotation){
                    Game game = new Game();
                    // 设置开放访问
                    method.setAccessible(true);
                    // 调用方法 第一个参数:实例化对象,第二个:注解传入的参数("startGame")
                    method.invoke(game,((MyAnnotation) annotation).info());
                    // 打印结果:游戏开始:startGame
                }
            }
    
        } catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException
                | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
    

    打印结果:

    类注解中的信息,type:0----info:Hello
    变量注解中的信息,type:1----info:int
    方法注解中的信息MyAnnotation
    游戏开始:startGame
    方法注解中的信息Deprecated
    

    重要方法:

    • boolean isAnnotationPresent(Class<? extends Annotation> annotationClass :传入注解类型,返回 class 是否持有该注解;
    • <T extends Annotation> T getAnnotation(Class<T> annotationClass):调用者获取所持注解,传入注解类型;
    • Annotation[] getAnnotations():获取调用者所有注解。

    有关反射的方法:

    • getDeclaredField():获取该类的属性,包括 public、private、protected 的;
    • getField():仅能获取类及其父类 public 的属性;
    • getDeclaredMethod():获取该类的 public、private、protected 方法。
    • getMethod():仅能获取到类及其父类 public 的方法
    • setAccessible(true):调用 private 方法或使用 private 变量的时候,需要设置允许访问权限,否则调用会报错。

    到这里,注解的定义、使用以及获取的基本流程就完成了。

    四、总结

    最后我们总结下注解的一些信息:

    Java 定义的用于编译期的注解:

    编译期
    所有指定保留策略为 RetentionPolicy.RUNTIME 的注解可以视为运行期注解。

    Java 定义的用于注解的注解:

    元注解

    开发者自定义注解:

    自定义注解步骤

    到此本文记录完毕,若有不当之处还望指出,不胜感激。

    相关文章

      网友评论

          本文标题:谈谈你对注解的理解

          本文链接:https://www.haomeiwen.com/subject/ffibdktx.html