谈谈你对注解的理解

目录

前言

刚写了一篇有关 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指定保留策略运行期
@Documentedjavadoc 工具生成文档运行期
@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 定义的用于注解的注解:

元注解

开发者自定义注解:

自定义注解步骤

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

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值