前言
刚写了一篇有关 CoordinatorLayout
和 AppBarLayout
的文章,里面有提到过 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.BaseBehavior extends... CoordinatorLayout.Behavior
这样就完成了 设置注解并传参 的过程。
3. 读取注解
注解已经添加到了 AppBarLayout
上,接下来看一下是如何读取并应用到代码逻辑里的。我们知道之前例子里 AppBarLayout
的 Behavior 是由 CoordinatorLayout
管理的,所以去源码里顺藤摸瓜:
CoordinatorLayout
的 onMeasure()
方法会调用一个 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.LayoutParams
的 setBehavior
方法把 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 :标记该注解具有继承性。被标记为可继承后,使用该注解的所有子类都可以获取到父类的注解信息。
实例:
- 首先我们准备两个注解,一个 @Inherited 标注,另一个没有。
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface TestAnnotation {
String value();
}
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation2 {
String value();
}
- 写一个类同时使用这两个注解。
@TestAnnotation("该注解具有继承性")
@TestAnnotation2("该注解不具有继承性")
public class Father {
}
- 再准备一个子类,继承于 Father 类。
public class Child extends Father {
}
- 进行测试:
public static void main(String[] args){
Class 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 containing annotation type 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 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 getAnnotation(Class annotationClass) -
Annotation[] getAnnotations()
:获取调用者所有注解。
有关反射的方法:
-
getDeclaredField()
:获取该类的属性,包括 public、private、protected 的; -
getField()
:仅能获取类及其父类 public 的属性; -
getDeclaredMethod()
:获取该类的 public、private、protected 方法。 -
getMethod()
:仅能获取到类及其父类 public 的方法 -
setAccessible(true)
:调用 private 方法或使用 private 变量的时候,需要设置允许访问权限,否则调用会报错。
到这里,注解的定义、使用以及获取的基本流程就完成了。
四、总结
最后我们总结下注解的一些信息:
Java 定义的用于编译期的注解:
所有指定保留策略为 RetentionPolicy.RUNTIME 的注解可以视为运行期注解。
Java 定义的用于注解的注解:
开发者自定义注解:
到此本文记录完毕,若有不当之处还望指出,不胜感激。