第十九章《类的加载与反射》第4节:注解

时间:2023-01-03 12:57:28

​在8.10小节曾经简单的介绍过注解,但当时只是简单的介绍了3个注解的作用,本小节将详细讲解注解的相关知识。注解始于JDK1.5,在Java语言中以Annotation接口表示注解。注解其实是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过使用注解,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证或者进行部署。

注解就像修饰符一样,可以用于修饰包、类、构造方法、普通方法、属性、参数以及局部变量,这些信息被存储在注解的“name=value”对中。需要注意:注解不影响程序代码的执行,无论增加、删除注解,代码都始终如一地执行。如果希望让程序中的注解在运行时起一定的作用,只有通过某种配套的工具对注解中的信息进行解析和处理,或者以反射的方式对注解信息做出专门解析和处理。​

19.4.1基本注解

在程序中如果需要使用注解,也需要向引入类一样进行引入操作。java.lang包下有5个注解,这5个注解不需要进行引入就能直接使用,因此它们也被称为“基本注解”。基本注解有:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs和@FunctionalInterface,这5个基本注解中有4个已经在第8章中进行了讲解,只有@SafeVarargs没有介绍过。​

@SafeVarargs注解用来抑制“堆污染”警告。什么是“堆污染”呢?在使用集合时,如果没有用泛型对集合进行元素类型限制有可能导致运行时出现异常,例如:​

List list1 = new ArrayList<Integer>() ;
list1.add(20) ;// ①添加元素时引发unchecked异常​
//下面代码引起“未经检查的转换"的警告,编译、运行时完全正常​
List<String> list2 = list1;//②​
//但只要访问list2里的元素,如下面代码就会引起运行时异常​
System.out.println(list2.get(0));//③​

以上代码中,语句②不会因list1和list2所指定的泛型不同而导致语法错误,因此语句②可以通过编译并能成功运行,但在运行③时则会出现异常。引起语句③在运行时出现异常的原因是编译器没有对语句②进行严格的泛型检查,导致没有指定泛型的集合引用能够指向指定了泛型的集合对象,语句②就是典型的这种情况。专业上,把指定了泛型的集合对象赋值给没有指定泛型的集合引用称为“堆污染”。​

从JDK1.7开始对可能产生的堆污染提出警告,但这种警告仅有一种情况,那就是方法参数的类型是泛型类且是可变参数,例如:​

public static void faultyMethod1(List<String>... listStrArray)//① ​
{
List[] listArray = listStrArray;//②​
listArray[0].add(new Integer(1));//③​
}
public static void faultyMethod2(List<String> list)//④ ​
{
List myList = list;//⑤​
}

在以上代码中,标记为①的位置会产生警告,警告的内容是“Type safety: Potential heap pollution via varargs parameter listStrArray”,意思是可变参数“listStrArray可能导致堆污染”。这个警告提示了程序员一种可能存在的风险,下面的语句②进行了堆污染的操作并不会被编译器判断为语法错误,而语句③则会导致运行时程序出现异常,因此警告的出现就是为了避免这种情况的发生。需要注意:编译器仅对参数是泛型类且是可变参数的情况发出堆污染警告,代码中标记为④的位置,参数list在语句⑤中赋值给原始类型的myList虽然也会导致堆污染,但并不会导致编译器发出警告。​

消除堆污染警告可以有两种方式,分别是使用@SafeVarargs和@SuppressWarnings("unchecked")进行压制。但@SafeVarargs使用上有比较严格的限制:​

  • @SafeVarargs只能用于方法,不能用于语句或类​
  • 方法必须被static或final关键字所修饰​
  • 方法中必须含有泛型类参数,且参数必须是可变类型​

以上代码中的faultyMethod1()方法就符合添加@SafeVarargs注解的条件,读者可以自己添加这个注解并观察消除警告的效果。​

19.4.2元注解

Java语言除在java.lang下提供了5个基本的注解之外,还在java.lang.annotation包下提供了6个元注解。元注解的作用是负责对他注解进行注解,也就是为其他注解提供了相关的解释说明。​

首先介绍的是@Retention,@Retention只能用于修饰注解定义,用于指定被修饰的注解可以保留多长时间。@Retention包含一个RetentionPolicy类型的value属性,所以使用@Retention时必须为该value属性指定值。value的值只能是以下三个,如表19-6所示。​

表19-6 @Retention的value值​

value值

意义

RetentionPolicy.CLASS

编译器将把注解记录在class 文件中。当运行Java程序时,JVM不可获取注解信息。这是默认值

RetentionPolicy.RUNTIME

编译器将把注解记录在class 文件中。当运行Java程序时,JVM也可获取注解信息,程序可以通过反射获取该注解信息

RetentionPolicy.SOURCE

注解只保留在源代码中,编译器直接丢弃这种注解

从表19-6可以看出:如果需要通过反射获取注解信息,就需要使用value 属性值为RetentionPolicy.RUNTIME 的@Retention。使用@Retention元注解可采用如下代码为value指定值。​

Retention (value= RetentionPolicy.RUNTIME )
public @interface​
Testable{ }

以上代码中,Testable是一个自定义的注解,在它上面的Retention注解表明Testable注解一直可被保留到程序运行时,如果希望注解仅在源代码中出现,编译时被丢弃,则可以把以上代码修改为:​

Retention (value= RetentionPolicy.SOURCE)
public @interface​
Testable{ }

实际上,指定注解value值时可以不用“name=value”的形式,也即可以把注解直接写为:​

Retention (RetentionPolicy.SOURCE)

接下来讲解@Target注解。@Target也只能修饰注解定义,它用于指定被修饰的注解能用于修饰哪些程序单元。@Target元注解也包含一个名为value 的属性,该属性的值只能是以下几个,如表19-7所示。​

表19-7 @Target的value值​

value

意义

ElementType.ANNOTATION _TYPE

指定该策略的注解只能修饰注解

ElementType.CONSTRUCTOR

指定该策略的注解只能修饰构造方法

ElementType.FIELD

指定该策略的注解只能修饰属性

ElementType.LOCAL_ VARIABLE

指定该策略的注解只能修饰局部变量

ElementType.METHOD

指定该策略的注解只能修饰方法定义

ElementType.PACKAGE

指定该策略的注解只能修饰包定义

ElementType.PARAMETER

指定该策略的注解可以修饰参数

ElementType.TYPE

指定该策略的注解可以修饰类、接口(包括注解类型)或枚举定义

与@Retention相同,使用@Target也可以直接在括号里指定value值,而无须使用“name=value”的形式。下面的代码指定@ ActionListenerFor注解只能修饰属性。​

@Target (ElementType.FIELD)
public @interface ActionListenerFor{ }

下面的代码片段指定@Testable注解只能修饰方法。​

@Target (ElementType.METHOD)
public @interface Testable { }

下面介绍@Document注解。@Documented用于指定被该元注解修饰的注解类将被javadoc工具提取成文档,如果定义注解类时使用了@Documented修饰,则所有使用该注解修饰的程序元素的API文档中将会包含该注解说明。​

下面代码定义了一个Testable注解,代码中使用@Documented来修饰@Testable注解定义,所以该注解将被javadoc工具所提取。​

@Retention (RetentionPolicy . RUNTIME)
@Target (E1 ementType.METHOD)
//定义Testable注解将被javadoc工具提取​
@Documented
public @interface Testable​
{
}

最后介绍@Inherited注解。@Inherited注解指定被它修饰的注解将具有继承性,即如果某个类使用了@Xxx注解(定义该注解时使用了@Inherited修饰)修饰,则其子类将自动被@Xxx修饰。​

下面使用@Inherited元注解修饰@Inheritable定义,则该注解将具有继承性。​

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ Inherited​
public @interface Inheritable​
{
}

以上程序中,自定义的Inheritable注解被@ Inherited注解修饰,这说明@Inheritable具有继承性,如果某个类使用了@Inheritable修饰,则该类的子类将自动使用@Inheritable修饰。​

19.4.3自定义注解

自定义注解类型使用@interface关键字,定义一个新的注解与定义一个接口非常像,下面的代码可定义一个简单的注解类型。​

public @interface Test{
}

定义了该注解之后,就可以在程序的任何地方使用该注解。使用注解的语法非常类似于public、final这样的修饰符,通常可用于修饰程序中的类、方法、变量、接口等定义。通常会把注解放在所有修饰符之前,而且由于使用注解时可能还需要为属性指定值,因而注解的长度可能较长,所以通常把注解另放一行,如下面的代码所示。​

//使用@Test修饰类定义​
@Test
public class MyClass{
}

在默认情况下,注解可用于修饰任何程序元素,包括类、接口、方法等,下面的代码是使用@Test来修饰方法。​

public class t MyClass{
//使用CTest注解修饰方法​
@Test
public void info(){
}
}

注解不仅可以是这种简单的注解,还可以带属性,属性在注解定义中以无形参的方法形式来声明,其方法名和返回值定义了该属性的名字和类型。如下代码可以定义一个有属性的注解。​

public @interface MyTag{ 
//定义带两个属性的注解​
//注解中的属性以方法的形式来定义​
String name() ;
int age() ;
}

一旦在注解里定义了属性之后,使用该注解时就应该为它的属性指定值,例如:​

public class Test{
//使用带属性的注解时,需要为属性赋值​
@MyTag (name="xx", age=6)
public void info(){
}
}

如果只有一个属性需要赋值,并且属性的名称是,value,则value可以省略,直接定义值即可,例如之前介绍的@ Retention注解就是如此。​

程序员也可以在定义注解的属性时为其指定初始值(默认值),指定属性的初始值可使用default关键字。下面的代码定义了@MyTag注解,该注解里包含了两个属性: name 和age,这两个属性使用default指定了初始值。​

public @interface MyTag{
//定义了两个属性的注解​
//使用default为两个属性指定初始值​
String name() default "Jack" ;
int age() default 22;
}

如果为注解的属性指定了默认值,使用该注解时则可以不为这些属性指定值,而是直接使用默认值。​

根据注解是否可以包含成员变量,可以把注解分为如下两类:​

  • 标记注解:没有定义成员变量的注解类型被称为标记。这种注解仅利用自身的存在与否来提供信息,如前面介绍的@Override、@Test 等注解。​
  • 元数据注解:包含成员变量的注解,因为它们可以接受更多的元数据,所以也被称为元数据注解。​

使用注解修饰了类、方法、成员变量等成员之后,这些注解不会自己生效,必须由开发者提供相应的工具来提取并处理注解信息。Java使用java.lang. annotation.Annotation接口来代表程序元素前面的注解,该接口是所有注解的父接口。JDK1.5在java.lang.reflect包下新增了AnnotatedElement 接口,该接口代表程序中可以接受注解的程序元素。该接口主要的实现类有:Class、Constructor、Field、Method、Package,它们分别代表类、构造方法、属性、普通方法、包。这些类在之前讲解反射相关知识时都已经讲解过,并且也讲解了如何通过反射的方式读取注解。下面的【例19_14】展示了自定义注解在实际开发过程中是如何标识某个方法是可测试的。​

【例19_14 自定义注解应用】

Exam19_14.java​

import java.lang.annotation.*;
import java.lang.reflect.*;
//使用@Retention指定注解的保留到运行时
@Retention(RetentionPolicy.RUNTIME )
//使用@Target指定被修饰的注解可用于修饰方法
@Target (ElementType.METHOD)
//定义一个标记注解,不包含任何成员变量,即不可传入元数据
@interface Testable{
}
class MyTest{
@Testable
public static void m1(){}
public static void m2(){}
@Testable
public static void m3(){ throw new IllegalArgumentException("参数异常!");}
public static void m4(){}
@Testable
public static void m5(){}
public static void m6(){}
@Testable
public static void m7(){throw new RuntimeException("业务出现异常!");}
public static void m8(){}
}
public class Exam19_14 {
public static void process (String clazz) throws ClassNotFoundException{
int passed = 0;
int failed = 0;
//遍历clazz对应的类里的所有方法
for (Method m : Class. forName (clazz) . getMethods() ) {
//如果该方法使用了@Testable修饰
if (m.isAnnotationPresent(Testable.class)) {
try {
//调用m方法
m.invoke(null);
//测试成功,passed计数器加1
passed++;
} catch (Exception e) {
System.out.println("方法" + m + "运行失败,异常:" + e.getCause());
// 测试出现异常, failed计数器加1
failed++;
}
}
}
//统计测试结果
System.out.println("共运行了:" + (passed + failed) + "个方法,其中"
+ "失败了" + failed + "个," + "成功了" + passed + "个! ");
}

public static void main(String[] args) throws Exception{
process("MyTest");
}
}

【例19_14】中,MyTest类有很多方法,那些被@ Testable注解标记的方法是可以测试的方法。在Exam19_14类的process()方法中用反射的方式获取了MyTest类中的所定义的方法,并依次检查这些方法是否有@ Testable注解,如果有@ Testable注解,则认为它是一个可以测试的方法,并以反射的形式运行了这些可测试的方法以检测这些方法是否能运行成功。【例19_14】的运行结果如图19-15所示。

第十九章《类的加载与反射》第4节:注解

图19-15【例19_14】运行结果​

通过上面例子读者不难看出,其实注解十分简单,它是对源代码增加的一些特殊标记,这些特殊标记可通过反射获取,当程序获取这些特殊标记后,程序可以做出相应的处理。​

本文字版教程还配有更详细的视频讲解,小伙伴们可以点击这里观看。