Java 基础之注解

概览

注解(Annotation),是从 Java 5 开始引入的一项新特性。所谓注解,就是一种元数据,可以藉此为程序添加非功能性的数据。注解对它们所标注代码的执行不会造成影响。

尽管注解对代码的执行不会影响代码的执行,但注解信息却可以嵌入编译生成的类文件,甚至在运行时得以保留。在 Java 源码中使用注释和 Javadoc 的标签也可以为程序添加一些描述信息,然而注解和注释信息却远不相同。

注解在 Java 开发,尤其是类库和框架开发中有很多应用。通常,注解的使用有以下三种方式:

  • 为编译器提供信息:可以被编译器用于错误检查和抑制警告
  • 编译和部署时进行处理:通过注解信息生成代码、配置文件等
  • 运行时进行处理:一些注解信息可以保留到运行时,通过反射进行获取

注解基础用法

注解的最简单形式就是使用 @Entity 的形式,用 @ 符号告知编译器紧跟着的是注解。比如我们最常使用的 @Override:

1
2
@Override
String toString(){ ... }

注解中可以包含元素,并可以为元素赋值:

1
2
@SuppressWarnings(value = "unchecked")
void myMethod() { ... }

在含有多个元素的情况时,用 , 进行分割:

1
2
3
4
5
@Author(
name = "Benjamin Franklin",
date = "3/27/2003"
)
class MyClass() { ... }

如果只有一个名为 value 的元素,可以忽略名称:

1
2
@SuppressWarnings("unchecked")
void myMethod() { ... }

可以同时叠加使用多个注解:

1
2
3
@Author(name = "Jane Doe")
@EBook
class MyClass { ... }

在 Java 8 之前,注解只能被使用在声明中:类、成员、方法以及其它编程元素的声明。在 Java 8 中,引入了类型注解 (type anotation) 的新特性,注解可以被应用于使用声明的任何地方,下面给几个例子:

类实例创建:

1
new @Interned MyObject();

类型转换:

1
myString = (@NonNull String) str;

实现接口:

1
2
class UnmodifiableList<T> implements
@Readonly List<@Readonly T> { ... }

异常:

1
2
void monitorTemperature() throws
@Critical TemperatureException { ... }

内置注解

Java 语言中内置了一些基本的注解,大致可以分为两类,一类通常用于 Java 代码中,供编译器使用;还有一类用来对注解进行注解,也称为元注解。

基本注解

Java 5 中包括三个基本注解,分别是 @Deprecated , @Override 和 @SuppressWarnings。

@Deprecated 表明被注解的元素已经被弃用,不应该在后续的代码中使用。通常,用 @Deprecated 注解元素的时候也会搭配 Javadoc 的 @deprecated 标签一起使用,从而在文档中进行说明。当程序中使用了被 @Deprecated 注解的类、方法或成员时,编译器会给出警告信息。

@Override 告知编译器被注解的元素覆盖了父类中的元素。@Override 不是必须的,但是使用时可以用来检查错误。如果用 @Override 注解的方法没有正确重写父类的方法(比如父类修改了相应的方法签名),编译器会给出错误信息。

@SuppressWarnings 告知编译器抑制特定的警告信息。例如,如果使用了一个被弃用的方法,可以用下面的方式消除编译时的警告信息:

1
2
3
4
5
6
7
8
// use a deprecated method and tell 
// compiler not to generate a warning
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
// deprecation warning
// - suppressed
objectOne.deprecatedMethod();
}

如果要抑制多类警告信息,可以这样使用:

1
@SuppressWarnings({"unchecked", "deprecation"})

@SafeVarargs 注解是 Java 7 开始引入的。当应用于方法或构造方法中时,假定代码不会在可变参数 (varargs ) 中执行不安全的操作。使用这个注解时,和可变参数相关的 unchecked 警告会被抑制:

1
2
3
4
5
6
7
@SafeVarargs // Not actually safe!
static void m(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // Semantically invalid, but compiles without warnings
String s = stringLists[0].get(0); // Oh no, ClassCastException at runtime!
}

@FunctionalInterface 注解是 Java 8 中引入的,表明被注解的接口是一个函数式接口。

元注解

元注解用于给注解添加注解,通常在自定义注解时会用到。Java 中定义了 5 个元注解,下面进行简单的介绍:

@Retention,指定注解的保留范围,可以将注解限定在源码级,也可以保留到运行时,可以有三个可用的值,在枚举 RetentionPolicy 中定义:

  • RetentionPolicy.SOURCE ,被标记的注解只会在源码级存在,会被编译器忽略
  • RetentionPolicy.CLASS ,被标记的注解会被编译器保留到 class 文件中,但会被 JVM 忽略(默认)
  • RetentionPolicy.RUNTIME ,被标记的注解会被 JVM 保留,并且可以在运行时使用(通过反射)

@Target,限定注解可以修饰的元素的类型,可用的值在 ElementType 中定义:

  • ElementType.ANNOTATION_TYPE ,可以被应用于注解类型
  • ElementType.CONSTRUCTOR ,构造方法
  • ElementType.FIELD ,成员
  • ElementType.LOCAL_VARIABLE ,局部变量
  • ElementType.METHOD ,方法
  • ElementType.PACKAGE ,包
  • ElementType.PARAMETER ,方法参数
  • ElementType.TYPE ,任意类型
  • ElementType.TYPE_PARAMETER, Java 8 新增,用于类型注解
  • ElementType.TYPE_USE, Java 8 新增,用于类型注解

@Inherited,表明注解可以从父类继承。默认情况下注解是不可继承的。

@Documented,表明注解在被应用于元素时,这些元素应该使用 Javadoc 生成文档。默认不使用。

@Repeatable,自 Java 8 开始引入,表明注解可以在一个元素上重复使用。

自定义注解

在对元注解有所了解后,我们就可以自定义注解了。自定义注解使用 @interface 关键字进行,类似于接口的声明。

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface MyAnotation {
String name();
int age();
}

可以为注解元素添加默认值:

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface MyAnotation {
String name() default "foo";
int age();
}

注解元素类似于方法,有类型和名称,其类型可以是:

  • 基本数据类型
  • 字符串
  • 注解
  • 枚举
  • 所有以上类型的数组

在使用注解时,我们需要为所有的注解元素设置值,有默认值的例外。

如果想让一个类和它的子类都包含某个注解,就可以使用 @Inherited 来修饰这个注解。

实际上注解就是一个特殊的接口,隐式地扩展了 java.lang.annotation.Annotation 接口,可以从反编译后的代码中一窥究竟:

1
2
3
4
5
6
7
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface MyAnotation {
String name() default "foo";
String[] value();
int age();
}

反编译 class 文件:

1
2
3
4
5
6
//Compiled from "MyAnotation.java"
public interface MyAnotation extends java.lang.annotation.Annotation {
public abstract java.lang.String name();
public abstract java.lang.String[] value();
public abstract int age();
}

注解处理

注解在很多框架和类库中都有使用,可以大幅度精简配置文件。对注解信息通常有两种处理方式,分别在运行时和编译期间进行处理。

如果注解信息被保留到了运行时,那么可以通过反射的方式来获取注解中的信息。在 java.lang.reflect 包中包含处理注解信息的相关类和接口。其中,AnnotatedElement 接口是获取注解信息的重要接口,一些反射类,如 Class, Constructor, Field, MethodPackage 都实现了该接口,通过该接口的 getAnnotations() 等方法可以获取被注解元素的注解,进而获得注解中的信息。

在运行时通过反射动态获取注解信息会对效率有一定的影响,另一种处理注解的方式是在编译期间进行处理。在 Java 源码被编译时,可以通过注解处理器 (annotation processors) 进行处理。注解处理器在编译时扫描代码中的注解信息,动态地生成文件或创建新的代码。在编译过程中,这一过程会持续多次,直到没有新的文件或代码生成。对注解处理器感兴趣的话可以看一下这篇文章

小结

文中对 Java 注解的基础知识进行了介绍。注解是一种特殊的接口,可以为被注解的元素(如类,方法等)提供额外的信息,但并不影响代码的执行。通过在编译器或运行时对注解进行处理,可以利用注解信息完成一些强大的功能,在很多框架中都有所体现。

-EOF-

参考
The Java™ Tutorials
Java深度历险(六)——Java注解
Java 技术手册