前言
官方注解的定义如下:
注解(一种元数据形式)提供有关不属于程序本身的程序的数据。注解对它们注解的代码的操作没有直接影响。
注解有许多用途,其中包括:
- 编译器的信息 - 编译器可以使用注解来检测错误或抑制警告。
- 编译时和部署时处理 - 软件工具可以处理注解信息以生成代码,XML文件等。
- 运行时处理 - 可以在运行时检查某些注解。
第一次看时候觉得"这尼玛是什么",于是换个简单一点的说法。
我们可以把注解当作是“标签”,就像是我们对某个人的看法一样,这个“标签”包含了对其中属性的评价(注解元素)。我们就可以对某个程序元素贴上这种"标签"(使用注解),既然能贴上去,那么我们自然也能通过某种方式(反射)来将这个"标签"撕下去(获取注解)。
有了这种比喻,相信下面对于注解的讲解就能更更容易理解了。一旦觉得理解不了注解是个什么东西,就回想“注解 ≈ 标签”就好了。
1. 基础知识
1.1 注解的格式
-
注解中无元素:
@Entity //如下,用于重写的 @Override 注解 @Override void myMethod(){...}
-
注解中有多个元素,且这些元素有值:
//这里的 @Author 是假定的自定义注解 @Author( name = "Benjamin Franklin", date = "3/27/2003" ) class MyClass() { ... }
-
注解中只有一个元素时:
//@SuppressWarnings 该注解是 Java 预定义的注解 @SuppressWarnings(value = "unchecked") void myMethod() { ... } //或者 可以将 value 省略 @SuppressWarnings("unchecked") void myMethod() { ... }
-
可以在同一声明中添加多个注解:
@Author(name = "Jane Doe") @EBook class MyClass { ... }
-
可以使用重复注解(使用相同类型的注解)
@Author(name = "Jane Doe") @Author(name = "John Smith") class MyClass { ... }
1.2 可以使用注解的位置
注解可以应用于声明以下内容:
- 类
- 字段
- 方法
- 其他程序元素
在 Java SE 8 以前,注解只能应用于声明。从 Java SE 8开始,添加了类型注解,具体示例如下:
-
创建类实例
new @Interned MyObject();
-
类型转换
myString = (@NonNull String) str;
-
implements 子句
class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
-
异常抛出语句
void monitorTemperature() throws @Critical TemperatureException { ... }
2. 声明注解类型
注解类型定义类似于接口定义,通过使用 @interface
关键字进行定义。
@interface MyAnnotation{
}
前面我们提到过,注解中可以有元素,注解元素是通过以下方式定义的:
@interface MyAnnotation{
int id() default 0;
String msg();
//...
}
这样定义好之后,我们就可以使用该类型的注解,并填入值,如下例子:
@MyAnnotation(id = 3, msg = "hello annotation")
public class Test {
//...
}
在定义 @MyAnnotation 注解时,我使用了一个关键字 default
,它是用来指定默认值的。当指定默认值之后,在我们使用注解并不填入值时,Java 会自动将其设为默认值。于是我们就可以如下调用:
@MyAnnotation(msg = "hello annotation")
public class Test {
//...
}
这里需要注意注解元素的类型是有限制的,注解元素只能使用如下类型:
- 基本数据类型
- String
- Class
- enum
- Annotation
- 以上类型的数组
3. 预定义注解
3.1 Java 使用的预定义注解
Java 使用的注解类型有如下几种:
-
@Deprecated
表示被注解的元素已弃用,不应再使用,只要程序使用带有 @Deprecated 注解的方法/类/字段,编译器就会生成警告。当不推荐使用时,也应使用 Javadoc @deprecated 对其标记。
// Javadoc comment follows /** * @deprecated * explanation of why it was deprecated */ @Deprecated static void deprecatedMethod() { }
-
@Override
该注解通知编译器被注解的元素要覆盖超类中声明的元素,具体实际上就是重写方法时使用。
// 将方法标记为已将超类方法覆盖 @Override int overriddenMethod() { }
需要强调一点:实际上,重写方法时 @Override 并非必须的,之所以使用该注解是为了防止出错。
当使用 @Override 注解的方法并没有正确覆盖其超类方法时,编译器会报错。
-
@SuppressWarnings
该注解告知编译器禁止可能生成的特定警告。举个例子,下面的示例中,使用了启用的方法,通常编译器会生成警告,但是使用 @SuppressWarnings 注解后,会导致该警告被禁止生成。
@SuppressWarnings("deprecation") void useDeprecatedMethod() { // deprecation warning // - suppressed objectOne.deprecatedMethod(); }
关于编译器警告,Java 语言规范指定了两个类别:
deprecation
和unchecked
deprecation
:使用了弃用方法时的类别unchecked
:当与泛型出现之前编写的遗留代码进行交互时,可能出现的警告类别。 -
@SafeVarargs
当该注解应用于方法或构造函数时,它保证了不会对可变参数执行不安全的操作。当使用该注解后,有关 可变参数的 unchecked 警告将被禁止生成。
-
@FunctionalInterface
在Java SE 8中引入的,指出类型声明的目的是作为一个函数式接口注解。
3.2 元注解
适用于其他注解的注解称为元注解 。
-
@Retention
指定注解的存储方式:
-
RetentionPolicy.SOURCE
- 标记的注解仅保留在源级别中,并被编译器忽略。 -
RetentionPolicy.CLASS
- 标记的注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略。 -
RetentionPolicy.RUNTIME
- 标记的注解由JVM保留,因此运行时环境可以使用它。
-
-
@Documented
作用是能够将注解中的元素包含到 Javadoc 中去
-
@Target
限制可以应用注解的 Java 元素类型:
ElementType.ANNOTATION_TYPE
可以应用于注解类型。ElementType.CONSTRUCTOR
可以应用于构造函数。ElementType.FIELD
可以应用于字段或属性。ElementType.LOCAL_VARIABLE
可以应用于局部变量。ElementType.METHOD
可以应用于方法级注释。ElementType.PACKAGE
可以应用于包声明。ElementType.PARAMETER
可以应用于方法的参数。ElementType.TYPE
可以应用于类的任何元素。
-
@Inherited
表明注解类型可以从超类继承。当超类使用了被" @Inherited 注解了的" 注解后,如果它的子类没有添加任何注解,那么子类会继承超类的注解。
比较绕,举个例子:
@Inherited @interface Test {} @Test public class A {} public class B extends A {}
上面,@Test 注解是可以被继承的,A 使用了 @Test 注解,B 继承 A,且未添加任何注解,于是 B 也就拥有 @Test 这个注解。
-
@Repeatable
Java SE 8 加入的特性,表明被注解的注解可以多次应用于相同的声明。
具体参见 重复注解 小节。
4. 重复注解
Java SE 8 中,加入了重复注解,这使得我们可以将相同的注解应用于声明或类型引用。
它的声明包含两个步骤:
- 声明可重复的注解类型
- 声明包含的注解类型
下面通过一个例子来介绍具体如何声明和使用重复注解:
第1步:声明可重复的注解类型
注解类型必须使用 @Repeatable 元注解进行标记:
@Repeatable(Schedules.class)
public @interface Schedule {
String dayOfMonth() default "first";
String dayOfWeek() default "Mon";
int hour() default 12;
}
其中, @Repeatable 元注解的值是Java编译器为存储重复注解而生成的容器注解的类型。此例中,包含的注解类型是 Schedules,因此 @Schedule 注解存储在 @Schedules 注解中。
第2步:声明包含的注解类型
包含的注解类型必须带有数组类型的 value 元素,数组的类型必须是可重复的注解类型:
public @interface Schedules {
Schedule[] value();
}
5. 检索 (Retrieving) 注解 - 注解与反射
前面我们所有讲解的都是如何声明和使用注解,那么此处就来讲讲怎么获取注解。
这主要是通过 反射 实现的,主要涉及以下 AnnotatedElement 接口提供的抽象方法 :
-
查询是否应用了某个注解
default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
-
获取注解
方法 功能 说明 <T extends Annotation> T getAnnotation(Class<T> annotationClass)
返回该程序元素上存在的指定类型的注解,如果该类型的注解不存在,则返回 null 泛型参数表明,只能说注解类型或者某注解类型的子类 Annotation[] getAnnotations()
返回该元素上存在的所有注解,包括继承得到的。如果没有注解,那么返回长度为零的数组 default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
Java SE 8 新增方法,用来获得修饰该程序元素的、指定类型的多个注解 default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
返回直接修饰该程序元素、指定类型的注解。如果该类型注解不存在,返回 null 忽略继承得到的注解 default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass)
Java SE 8 新增方法,用来获得直接修饰该程序元素的、指定类型的多个注解 忽略继承得到的注解 Annotation[] getDeclaredAnnotations()
返回直接存在于此元素上的所有注解。如果没有注解,返回长度为零的数组。 忽略继承得到的注解
下面具体来看个例子:
MyAnnotation.java
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)//指定注解保存在JVM 中,运行时可使用
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})//注解可以应用于类、方法和字段上
public @interface MyAnnotation{
String type() default "ignore";
String[] hobby();
}
TestAnnotation.java
@MyAnnotation(type = "class",hobby = {"sleep","play"})
public class TestAnnotation {
@MyAnnotation(type = "Field",hobby = {"read"})
private String whdalive;
@MyAnnotation(type = "Field",hobby = {"piano"})
private String cy;
@MyAnnotation(type = "Method",hobby = {"guitar"})
public void method1(){
}
public static void main(String[] args) {
//反射获取类对象
Class<TestAnnotation> clz = TestAnnotation.class;
//类上是否含有指定类型注解
boolean clzHasAnno = clz.isAnnotationPresent(MyAnnotation.class);
//如果有,获取注解元素的值
if(clzHasAnno) {
MyAnnotation annotation = clz.getAnnotation(MyAnnotation.class);
String type = annotation.type();
String[] hobby = annotation.hobby();
System.out.println(clz.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
}
//反射获取字段
Field[] fields = clz.getDeclaredFields();
for(Field field : fields) {
boolean fieldHasAnno = field.isAnnotationPresent(MyAnnotation.class);
if (fieldHasAnno) {
MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
String type = annotation.type();
String[] hobby = annotation.hobby();
System.out.println(field.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
}
}
//反射获取方法
Method[] methods = clz.getDeclaredMethods();
for (Method method : methods) {
boolean methodHasAnno = method.isAnnotationPresent(MyAnnotation.class);
if (methodHasAnno) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
String type = annotation.type();
String[] hobby = annotation.hobby();
System.out.println(method.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
}
}
}
}
输出结果
com.whdalive.reflection.TestAnnotation, type = class, hobby = [sleep, play]
whdalive, type = Field, hobby = [read]
cy, type = Field, hobby = [piano]
method1, type = Method, hobby = [guitar]
总结
注解是 Java 引入的一个非常受欢迎的机制,它提供了一种结构化的并且具有类型检查能力的新途径。我们可以通过注解为代码加入元数据,而不会导致代码杂乱难以阅读。
同时,注解还是 Android 诸多开源框架的实现基础。比如 ButterKnife、Retrofit、Dagger2 等等等等。
注解还是很有必要好好学习一下的。
共勉。