注解本身是一种标记,科学的叫法叫做“元数据”;
本身并不会对代码的运行产生任何影响,如果只使用注解本身,甚至和注释没有区别。
只有与反射合作,才能发挥出注解的作用。
常见注解
最常见可能就是@Override了,表示覆写了方法,看源码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
或者@Deprecated,表示废弃,看源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
注解的写法就是这样,没有过多的东西。
写法很类似接口,不过也有细微差别----比如“@",比如可以为属性设置默认值等。
基础语法
以上面的Override注解为例,声明了两个属性,一个Target,一个Retention,其实属性种类并不只有这两种。也可称之为“元注解”。
一共有4种:
属性 | 说明 |
---|---|
Documented | 是否加入JavaDoc |
Retention | 说明保留时段(生命周期) |
Target | 说明作用域(可用范围) |
Inherited | 是否继承 |
通过源码可以看到),其中Documented和Inherited并没有属性(方法),只需要选择是否添加;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
而Target和Retention则需要相应的值才能发挥应有的作用;
首先是Target:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
ElementType是一个枚举类,它有一系列描述作用域的值:
属性 | 说明 |
---|---|
ElementType.TYPE | 作用于类、接口、枚举等 |
ElementType.FIELD | 作用于成员变量 |
ElementType.METHOD | 作用于方法 |
ElementType.PARAMETER | 作用于参数 |
ElementType.CONSTRUCTOR | 作用于构造方法 |
ElementType.LOCAL_VARIABLE | 作用于本地(临时)变量 |
ElementType.ANNOTATION_TYPE | 作用于注解类型 |
ElementType.PACKAGE | 作用于包 |
ElementType.TYPE_PARAMETER | 作用于泛型参数 |
ElementType.TYPE_USE | 作用于泛型 |
再来是Retention:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
RetentionPolicy同样是一个枚举,有三个描述时段的值:
属性 | 说明 |
---|---|
RetentionPolicy.SOURCE | 源码阶段保留,指仅在java文件中保留,编译即丢弃注解 |
RetentionPolicy.CLASS | class文件保留,指类加载时保留,运行时丢弃 |
RetentionPolicy.RUNTIME | 运行时保留,指一直保留 |
此时回过头来看@Override,发现其Target为ElementType.METHOD,即代表了只作用于方法,而Retention为RetentionPolicy.SOURCE,意思是只在java文件中保留,编译和运行时,这个注解已经不存在了。
相应地,Deprecated不仅保留到了运行时阶段,而且作用域非常大,可广泛用于变量、方法、类中。
虽然注解本身不影响运行,但不要去尝试跨越作用域使用注解,编译器会死死拦着你的,就像这样:
这不是warning,是error。
定好了只作用于方法,在类上使用就会被红牌error。
相反,Deprecated作用域宽广,自然就可以为所欲为了:
当然,它也不是真的就可以为所欲为的,毕竟我们看到它可以作用于7个域,但总共有10个域呢(Java8的情况下),其中ANNOTATION_TYPE、TYPE_USE、TYPE_PARAMETER就不在其中,也就是说,Deprecated不能注解“注解类型”“泛型参数”“泛型”三种作用域,就像这样:
这是TYPE_USE,也就是泛型的作用域,明显会报错;
再比如这样:
这是泛型参数的作用域,也就是TYPE_PARAMETER,也会报错;
注解就不说了,本文记载的就是注解。
就注解本身的四种属性而言,每个都指定了@Target(ElementType.ANNOTATION_TYPE),即代表着仅注解类型可用,包括Target自己。
作用域就是如此严格限制了注解的作用范围,而且是在编译时就已经限制了。
了解了“保留时段”与“作用域”,接下来也只有参数的写法可说了。
写法也很简单,就像写接口方法一样,不同的是可以设置默认值。
举个例子:
@Documented
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Caution {
String value1() default "m";
int value2() default 10;
String value3();
}
那么在使用注解标记某个作用域时,就是这样的:
@Caution(value1 = "kk", value2 = 2, value3 = "n")
public void work() {
}
值得注意的是,设置默认值后就代表着可以不用赋值了,也就是说,只需要对未设置默认值的属性赋值,也就是这样:
@Caution(value3 = "n")
public void work() {
}
上面这样写就等价于:
@Caution(value1 = "m", value2 = 10, value3 = "n")
public void work() {
}
这太好理解了,其实都不用记下来的…
就像重载了多个构造方法一样,如果所有属性均有默认值,自然就不需要传入参数,仅此而已。
使用方法
基础语法已经知道,注解类本身很容易就能写出来,使用注解也很容易,在作用域上添加相应注解就可以,那么怎样才能发挥作用呢?
注解是一种标记,那么标记后呢?
标记的作用,是让使用者更容易找到。
注解结合反射,就能使用反射很容易找到被注解标记的方法或类或属性,然后对其进行后续的操作。
比如定义这样一个注解类:
@Documented
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.PARAMETER,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Caution {
String value1() default "m";
int value2() default 10;
String value3();
}
那么使用的时候就需要通过反射去把注解找出来(这就是注解对应的主要“业务”),这里把注解和查找注解的业务逻辑写在同一个类里,就像这样:
@Caution(value3 = "type")
public class CautionTest {
@Caution(value3 = "field")
private String example1;
private String example2;
public static void main(@Caution(value1 = "ha", value3 = "params") String[] args) {
Class ct = CautionTest.class;
//判断类本身是否被Caution注解
System.out.println("class has Caution: " + ct.isAnnotationPresent(Caution.class));
System.out.println("--------------------");
System.out.println("which method has Caution: ");
Method[] methods = ct.getDeclaredMethods();
for (Method m : methods) {
//判断方法是否被Caution注解
if (m.isAnnotationPresent(Caution.class)) {
Caution caution = m.getAnnotation(Caution.class);
System.out.println(" method name is " + m.getName() + ", value1=" + caution.value1() + ", value2=" + caution.value2() + ", value3=" + caution.value3());
}
Parameter[] parameters = m.getParameters();
if (parameters != null && parameters.length > 0) {
for (Parameter p : parameters) {
//判断参数是否被Caution注解
if (p.isAnnotationPresent(Caution.class)) {
Caution caution = p.getAnnotation(Caution.class);
System.out.println(" params type is " + p.toString() + ", value1=" + caution.value1() + ", value2=" + caution.value2() + ", value3=" + caution.value3());
}
}
}
}
System.out.println("--------------------");
System.out.println("field has Caution: ");
Field[] fields = ct.getDeclaredFields();
for (Field f : fields) {
//判断成员变量是否被Caution注解
if (f.isAnnotationPresent(Caution.class)) {
Caution caution = f.getAnnotation(Caution.class);
System.out.println(" field name is " + f.getName() + ", value1=" + caution.value1() + ", value2=" + caution.value2() + ", value3=" + caution.value3());
}
}
}
@Caution(value3 = "method")
public static void work() {
System.out.println("I'm working");
}
public static void study(@Caution(value1 = "study", value2 = 3, value3 = "params") String subject) {
System.out.println("I'm studying " + subject);
}
}
可以得到输出:
class has Caution: true
--------------------
which method has Caution:
params type is java.lang.String[] arg0, value1=ha, value2=10, value3=params
params type is java.lang.String arg0, value1=study, value2=3, value3=params
method name is work, value1=m, value2=10, value3=method
--------------------
field has Caution:
field name is example1, value1=m, value2=10, value3=field
通过反射,就能找到注解所在的类、方法或变量,这样就已经达到了本质目的。
常见使用
许多常用的第三方开源库或框架都大量使用了注解,举个例子,EventBus。
EventBus使用@Subcribe注解标记某个方法,使得此方法可以接收post事件,而这个过程是在register时,由反射找到注解方法并建立映射关系的,而其中findUsingReflectionInSingleClass就是实现些功能的重要方法之一,来看源码:
//EventBus.java
private void findUsingReflectionInSingleClass(FindState findState) {
Method[] methods;
try {
// This is faster than getMethods, especially when subscribers are fat classes like Activities
methods = findState.clazz.getDeclaredMethods();
} catch (Throwable th) {
// Workaround for java.lang.NoClassDefFoundError, see https://github.com/greenrobot/EventBus/issues/149
methods = findState.clazz.getMethods();
findState.skipSuperClasses = true;
}
for (Method method : methods) {
int modifiers = method.getModifiers();
if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 1) {
Subscribe subscribeAnnotation = method.getAnnotation(Subscribe.class);
if (subscribeAnnotation != null) {
Class<?> eventType = parameterTypes[0];
if (findState.checkAdd(method, eventType)) {
ThreadMode threadMode = subscribeAnnotation.threadMode();
findState.subscriberMethods.add(new SubscriberMethod(method, eventType, threadMode,
subscribeAnnotation.priority(), subscribeAnnotation.sticky()));
}
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException("@Subscribe method " + methodName +
"must have exactly 1 parameter but has " + parameterTypes.length);
}
} else if (strictMethodVerification && method.isAnnotationPresent(Subscribe.class)) {
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
throw new EventBusException(methodName +
" is a illegal @Subscribe method: must be public, non-static, and non-abstract");
}
}
}
可以很清楚地看到,这里遍历了一个类的所有方法,并指定了筛选条件:1、只要public修饰的方法;2、只要参数个数为1个的方法;3、只要被Subscribe注解的方法。
其他使用注解的开源库或代码均有类似操作,注解为筛选过程提供了相当大的便利。
了解更多
- 注解本身是继承于Annotation接口的
public @interface Override {
}
等同于
public interface Override extends Annotation{
}
而Annotation接口位于JDK源码java.lang.annotation包内:
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
从方法提示中得到证据:
可以看到,注解本身是继承了Annotation的方法的。
- 注解本身可以自我注解的。如果需要注解其他注解类,只需要将Target设置为ANNOTATION_TYPE即可,注解可称为“元数据”,那么这种可以注解其他注解类的注解,可称为“元注解”。可以自行定义一个“元注解”。当然,因为没有写更多的使用代码(业务逻辑),这只是名义上的“元注解”。
@Self
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Self {
}
- 注解总是需要搭配相应的“业务”才能发挥作用的。“元注解”如Target、Retention等,内置注解如Override的“业务”都在Java的编译器javac中,所以源码中是找不到踪影的。许多时候,我们将注解对应的“业务”集合到一个工具中,这个工具就是“注解编译器”,也叫“注解处理器”。许多开源库,也是需要搭配相应的注解编译器方可使用。
比如ButterKnife的使用,需要导入:
implementation 'com.jakewharton:butterknife:(insert latest version)'
annotationProcessor 'com.jakewharton:butterknife-compiler:(insert latest version)'
- Class类是实现了AnnotatedElement接口的,这个接口允许反射性地读取注解,因此类总是可以通过反射找到其注解。