1 Java注解是什么?

根据官方文档说明,Java注解是元数据的一种形式,它不直接影响我们代码本身的操作,但是有以下几个用途:

  • 给编译器提供信息-编译器可以用注解来检测错误或者抑制编译时的warning信息
  • 编译和部署代码时进一步加工—一些工具可以根据注解生成额外的代码或文件
  • 运行时处理:一些注解可以在运行时被检测

2 注解的形式和定义

注解是以@符号开头,后接注解名称,如果有参数的话,后面再用()把参数写进去,参数都是key=value的形式传递,只有一个参数时默认是对应value这个key,可以不写。 如:

1
2
3
4
5
6
7
8
9
//无参数
@Override
void mySuperMethod() { ... }
//一个参数
@SuppressWarnings(value = "unchecked")
void myMethod() { ... }
//一个参数时可以省略key
@SuppressWarnings("unchecked")
void myMethod() { ... }

如何定义一个注解:
注解本质上是interface的一种形式,只是在interface关键字前加了个@符号,如:

1
2
3
4
5
6
7
8
9
@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   // Note use of array
   String[] reviewers();
}

3 Java SE API 预定义的注解

预定义的注解有一些是给编译器使用的,另一些是给其它注解使用的(元注解)。

3.1 编译器用的注解

这些注解的定义本身也有元注解,后面再讲。

@Deprecated:

1
2
3
4
5
6
7
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    String since() default "";
    boolean forRemoval() default false;
}

表示一个元素(类、方法或属性)被废弃。语言和框架等一直在更新,一些元素因为安全或者性能原因在新版本中被新的类或方法取代,此时旧的方法不能直接删除(为了保持兼容性),但又不建议使用,所以标记为@Deprecated,当编译器编译时,如果发现你的代码中使用了标记为@Deprecated的元素,就会生成warning

@Override:

1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

表示一个方法是重载父类的方法或者实现接口的方法。主要是避免方法名称拼写错误,如果出现了拼写错误,编译器会报错。现在IDE越来越智能,这类错误很少出现了。

@SuppressWarnings:

1
2
3
4
5
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

让编译器不要显示warning。比如一个方法调用了另一个标记为deprecated的方法,正常编译器会生成warning,用@SuppressWarnings("deprecation")可以不让其显示。warning有很多种,比如deprecationunchecked,可以选择同时阻止多个warning@SuppressWarnings({"unchecked", "deprecation"})

@SafeVarargs

1
2
3
4
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs {}

1.7引入
作用对象:变长参数的【构造函数或者方法】,方法必须是final或者static,java9开始也可以是private,这个限制主要是为了不让方法被重载。如果参数是固定长度的,或者方法的修饰符不满足上面的要求,编译器都会报错。

效果:(程序员)声明函数体本身不会对变长参数进行潜在的不安全操作,让编译器不生成unchecked warning

什么是对变长参数的潜在不安全操作呢?简单说明一下:

查看详细内容

当你的变长参数跟泛型有关(non-reifiable)的时候,就可能会出现类型转换的安全问题。用官方API的例子来解释:

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!
 }

这是一个不正确的使用@SafeVarargs注解的例子,因为这段代码会抛出运行时异常。一行行分析:

首先,方法传入了一个变长参数,形式是List<String>... stringLists,变长参数在编译时会转化为数组,所以,这个参数在转化后就变成List<String>[] stringLists,Java不是不支持泛型数组吗???事实上,对于可变参数使用泛型的这种形式是支持的,所以它也会导致那些泛型数组会带来的问题。

Object[] array = stringLists;
然后我们创建了一个Object[]数组变量array,并且初始化为stringLists,这里因为类型擦除,所以在编译后stringLists指向的类型是List<Object>[]。因为数组赋值是传递的数组对象的引用,所以array也是指向的stringLists的地址,也就是List<Object>[]这里是伏笔

List<Integer> tmpList = Arrays.asList(42);
这一行新建一个List<Integer>,里面只有一个元素42

array[0] = tmpList;
把上面新建的这个List存到array[0]的位置。因为前面的类型擦除,所以这里是把一个List<Integer>对象放到List<Object>[0]里面,编译时没有问题,运行到这一步也没有问题。如果我们的参数stringLists不是泛型,而是普通的类型比如String,修改例子中的相应代码,那么这一步就会报运行时错误ArrayStoreException

String s = stringLists[0].get(0);右边读取的是Integer类型的42,想要转化成String失败,抛出运行时异常,在这一步抛出异常并不清晰,直到使用数组的元素时我们才发现存错了,如果代码复杂一点,会非常难以排查。

概括一下:实际上,“潜在的不安全操作"有很多种情况,大致都是由于数组和泛型的混用引起。@SafeVarargs可以在方法源头阻止编译器生成unchecked warning,否则的话需要在每个调用方使用@SuppressWarnings来抑制这些warning。使用@SafeVarargs时要注意几点:

  • 只有在你真的确定代码是安全的情况下才使用@SafeVarargs,不然会让你的异常更加排查,通常有两个准则需要遵守:
    • 不直接修改泛型数组变量,只读不改
    • 不对外开放泛型数组变量的引用,本质也是同上
  • 可以考虑用List来替换可变参数,虽然代码会冗长一点,也更安全。

@FunctionalInterface

1
2
3
4
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

1.8引入
表明一个接口是一个函数接口(有且仅有一个抽象方法的接口),如果不满足这个定义,或者这个注解不是被用在接口上,编译器都会报错。
事实上,就算没有这个注解,只要接口满足函数接口的要求,编译器也会把它当成函数接口,所以这个注解提供了一个检测功能。

@Native

1
2
3
4
5
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface Native {
}

1.8引入
用在一个定义常量的字段上,表明这个字段的值可能引用native code(比如平台相关的底层库)。一些生成native code头文件的工具通过这个注解来判断是否需要生成相应的头文件,以及在头文件中应该包含哪些声明。这个注解用的少,通常跟JNI有关。

3.2 作用在注解上的注解

下面的这些注解只用在其它注解定义时,被称为元注解

@Retention

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

表明注解在哪个阶段(含)之前持续存在,接收一个参数value,类型是RetentionPolicy,这是一个Enum类型,包含3个值,如果没有指定value的值,默认是RetentionPolicy.CLASS

  • RetentionPolicy.SOURCE:表示注解只存在源代码中,在编译器使用后就抛弃,不写入.class文件。上面介绍过的@Override,@SuppressWarnings都是SOURCE级别
  • RetentionPolicy.CLASS:表示注解存在于.class文件中,但虚拟机执行时会抛弃这些注解
  • RetentionPolicy.RUNTIME:表示注解一直存在,包括虚拟机执行时。@SafeVarargs,@FunctonalInterface都是RUNTIME级别,主要是为了可以在运行时通过反射获取注解信息

CLASSRUNTIME的区别:
两者都在字节码文件可见,区别是CLASS会生成一个RuntimeInvisibleAnnotations,而RUNTIME会生成RuntimeVisibleAnnotations,就是注解是否对虚拟机可见,不过这个行为可以被命令行参数覆盖。
通常都直接使用RUNTIME或者SOURCECLASS的适用场景并不多

@Documented

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

当在一个注解上标记@Documented时,表示所有使用这个注解的对象在生成javadoc时都会显示这个注解,默认情况javadoc生成时是不包含注解的。如果一个对象有多个注解,那么生成javadoc时是否显示这些注解是根据每一个注解自身是否使用了@Documented分别决定的。

@Target

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

表示注解可以作用的对象。接收一个ElementType[]ElementType是一个Enum类型,可以有如下取值(基于jdk11):

  • ElementType.TYPE:所有class,inteface,enum类型的声明,其中inteface也包括了注解
  • ElementType.FIELD:字段的声明,包括Enum里面的常量
  • ElementType.METHOD:方法的声明
  • ElementType.PARAMETER:形参的声明
  • ElementType.CONSTRUCTOR:构造函数的声明
  • ElementType.LOCAL_VARIABLE:本地变量的声明
  • ElementType.ANNOTATION_TYPE:注解的声明
  • ElementType.PACKAGE:package的声明
  • ElementType.TYPE_PARAMETER:1.8引入,类型参数的声明,如public <@MyAnnotation T> T retT(T t){ return t; }
  • ElementType.TYPE_USE:1.8引入,TYPE可以用的地方它都可以用。在1.8之前,注解只能用在“声明”的时候,有了TYPE_USE,可以在任何跟Type有关的地方使用它。官方提供了以下几个例子:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Class实例创建时:
    new @Interned MyObject();

类型转换时:
    myString = (@NonNull String) str;

implements语句里:
    class UnmodifiableList<T> implements
        @Readonly List<@Readonly T> { ... }

Thrown exception的声明语句里:
    void monitorTemperature() throws
        @Critical TemperatureException { ... }
  • ElementType.MODULE:1.8引入,module的声明

@Inherited

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

如果一个注解被标记为@Inherited,那么使用了这个注解的类在被继承的时候,类上的注解也会一起被“继承”。这里注解的“继承”表现为:在子类上可以通过反射获取到自身没有但父类有的注解。
一个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
//定义一个注解,标记为@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Inherited
public @interface MyAnnotation {
}


//测试类
public class Test{
    public static void main(String[] args) {
      //从子类获取注解信息
        System.out.println(SubTest.class.isAnnotationPresent(MyAnnotation.class));//true
    }
}
//父类使用了注解
@MyAnnotation
class ParentTest{}
//子类没有使用注解
class SubTest extends ParentTest{}

注意:@Inherited只在class上生效,interface无效。

@Repeatable

1
2
3
4
5
6
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    Class<? extends Annotation> value();
}

1.8引入。
表示一个注解可以在同一个地方被使用多次。接收一个参数Class<? extends Annotation> value(),这是一个注解类型的Class,要解释这个,先从没有@Repeatable的版本说起。
在1.7版本,同一个注解不能在一个地方出现两次,为了实现类似功能,需要用另一个注解的数组参数把两个相同的注解“封装”起来。比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//Schedule注解
public @interface Schedule {
    String dayOfMonth() default "first";
    String dayOfWeek() default "Mon";
    int hour() default 12;
}
//Schedules注解,接收一个参数@Schedule[],注意这里声明变量时Schedule前不加@
public @interface Schedules {
    Schedule[] value();
}

//调用@Schedules,间接调用多个@Schedule
@Schedules(value = {@Schedule(dayOfMonth = "last"),@Schedule(dayOfWeek = "Fri",hour =23)})
public void doSth(){}

1.8以后上面的代码可以写成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Repeatable(Schedules.class)
public @interface Schedule {
    String dayOfMonth() default "first";
    String dayOfWeek() default "Mon";
    int hour() default 12;
}

public @interface Schedules {
    Schedule[] value();
}

@Schedule(dayOfMonth = "last")
@Schedule(dayOfWeek = "Fri",hour =23)
public void doSth(){}

对比可以发现,依然需要3个步骤,不同的是@Schedule定义的时候多了一个@Repeatable(Schedules.class)注解,这一步是指明我们用了哪一个注解来包装我们的@Schedule,这里是@Schedules,所以传入Schedules.class,然后我们就可以多次使用@Schedule了,看上去repeatable
实际上,这只是语法糖,通过编译后的class文件我们可以发现,两个版本都有如下相同内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
RuntimeInvisibleAnnotations:
  0: #17(#18=[@#19(#20=s#21),@#19(#22=s#23,#24=I#25)])
    Schedules(
      value=[@Schedule(
        dayOfMonth="last"
      ),@Schedule(
        dayOfWeek="Fri"
        hour=23
      )]
    )

这个形式跟我们在1.7版本没有引入@Repeatable之前是一样的

4 关于自定义注解

前面介绍的JAVA原生支持的注解,直接使用就有效果。自定义注解不一样,你直接使用一个自定义注解的时候,编译器和虚拟机无法理解你想表达什么,你需要自己写"注解解析器”,根据注解信息执行相应操作,而注解信息则通过反射获取,所以自定义注解通常要标记为@Retention(RetentionPolicy.RUNTIME)Spring等框架就大量使用了自定义注解。