学习目标:注解可以为代码提供元数据,利用这些元数据。在编译阶段,编译器可以利用注解在编译过程中对代码进行检查。在程序运行阶段,可以利用反射技术解析注解并提取元数据,进行相应的操作和处理。主流的第三方Java开发框架基本都是用注解与反射技术来实现的。
认识注解
注解(Annotation)可以添加到类、方法、变量、参数的声明前面,类似于注释。不过注解和注释是截然不同的两个概念。
注释仅对代码起到解释作用,方便程序员阅读代码。
注解虽然也能对代码起到解释作用,但注解的主要目的不是对代码进行注释。注解可以为代码提供元数据,利用这些元数据,注解可以起到如下作用:
(1)为编译器提供额外的代码信息;
(2)编译器可以利用注解在编译过程中对代码进行检查;
(3)在程序运行过程中,可以利用反射技术解析注解并提取元数据,进行相应的操作和处理;
(4)javadoc可以利用注解提供的元数据生成与源代码配套的帮助文档。
元数据是描述数据的数据,在这里可以理解为描述代码的数据,为代码提供额外的数据。例如,在前面课程的案例代码中,当子类方法重写父类方法时,会要求子类在重写父类方法的声明前面添加“@Override”,“@Override”就是一个已定义并用于编译检查的注解,这是一个标记注解,注解的元数据就是自身。在编译过程中,当编译器检测到某个方法添加了“@Override”注解,编译器就会检查当前方法在父类是否存在,如果父类没有该方法,编译器就会给出警告信息。
Java主流开发框架Spring内部就定义了大量注解,程序员可以在编写的Java代码中使用这些注解。当Spring加载类时会扫描这些注解,并根据注解内容进行程序的装配。
注解主要分为四大类:一类是来自Java内部定义的注解,也称为内置的注解,如@Override、@Deprecated等注解;一类是第三方框架定义的注解,如Spring框架的@Autowired、@Service等注解;一类是程序内部定义的注解,编写程序时自己来定义注解;一类是元注解,元注解在Java中也被定义,元注解主要用于对自定义的注解进行描述,也可以理解为注解的注解。
如何在程序内部定义注解呢?
在Java语言中,注解也是一个类,注解和类的定义基本相同,注解也有属性,但没有方法。注解的定义语法如下:
public @interface 注解名称
{
类型 属性名称() default 默认值;
……
}
@interface是定义注解的修饰符,注解名称是自定义注解的名字,命名方式和类的命名方式相同。注解的主体内容由一对“{}”括起来,主体内容主要存放注解的属性,注解可以有多个注解属性,也可以没有注解属性,没有注解属性时,“{}”括号内不需要写任何内容。
注解属性的定义类似类方法的定义,属性名称前面是属性的类型,类型可以是基本数据类型、String、Class、枚举,也可以是前面类型的数组。类型后面是属性名称,类型和属性名称之间需要用空格隔开,紧跟属性名称是一对“()”括号,括号内不需要写任何内容。如果需要对属性设置默认值,可以在属性名称后面使用default关键字来设置属性的默认值。
案例15:建立Rate注解,Rate注解为其它类提供银行定期存款利率元数据。
在PUnit13项目新建annotation包,在annotation包下新建Rate注解。创建Rate类时,选择注解类型。

Rate类代码如下:
package annotation;
public @interface Rate {
// 利率默认值0.03
double rate() default 0.03;
}
Rate是注解的名称,Rate注解内部定义了一个double类型的属性rate,该属性的默认值为0.03。Rate注解可以为其它类提供银行定期存款利率元数据。
既然Rate注解为其它类提供银行定期存款利率数据,那么注解是如何使用的呢?其它类是如何读取注解并获取注解属性呢?这就要用到Java的反射技术。
使用注解
注解定义完后后,就可以使用注解了。注解的使用非常简单,注解可以添加在类、成员方法、成员变量、参数声明前面,每个类、成员方法、成员变量前面可以添加多个注解。注解添加语法如下:
@注解名称(属性名称1=值1,属性名称2=值2,……)
当注解有多个属性需要赋值时,每个属性赋值语句之间用英文逗号分隔。
例如:
@Rate(rate=0.035)
double dRate;
注解Rate的属性rate被赋值为0.035,该注解添加在变量dRate的前面,因此该注解作用于dRate变量。
如果注解的属性类型是数组,赋值方式为:
@Demo(value={20,30},name=”lihua”)
public void showDemo() {};
其中,Demo为注解名称,该注解有两个属性,分别是value属性和name属性,value属性是int类型的数组,name属性是String类型。
Demo注解添加在showDemo()方法的前面,因此该注解作用于showDemo()方法。
在程序运行过程中,如果要读取注解和注解内部定义的属性,只能使用反射技术,如果没有反射技术,注解的作用就大打折扣了。
与反射技术相关的Class类、Constructor类、Method类、Field类都定义了一些方法用于操作注解。
Class类定义的常用注解方法说明如下:
● Annotation[] getAnnotations()
该方法用于返回Annotation类型的数组,数组包含类内部的所有注解。
● <A extends Annotation> A getAnnotation(Class<A> annotationClass)
该方法用于返回类中指定类型的注解,类型由annotationClass指定,如果没有该类型的注解,返回空。
类Constructor、Field和Method均继承了AccessibleObject类,该类定义了用于注解操作的方法,该类常用的注解操作方法说明如下:
● Annotation[] getAnnotations()
该方法用于返回Annotation类型的数组,数组包含当前元素上的所有注解。
● <T extends Annotation> T getAnnotation(Class<T> annotationClass)
该方法用于返回当前元素上指定类型的注解,类型由annotationClass指定,如果没有该类型的注解,返回空。
● <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass)
该方法用于返回扩展Annotation类型的数组,该数组包含当前元素上指定annotationClass类型的注解。如果当前元素上没有该类型的注解,返回长度为0的空数组。
● Annotation[] getDeclaredAnnotations()
该方法返回Annotation类型的数组,数组包含当前元素上直接出现的所有注解,此方法忽略继承的注解。如果当前元素上没有注解,则返回值为长度为0的数组。
● <T extends Annotation> T[] getDeclaredAnnotationsByType (Class<T> annotationClass)
该方法用于返回扩展Annotation类型的数组,该数组包含当前元素直接出现的类型为annotationClass的注解。如果当前元素上没有该类型的注解,返回长度为0的空数组。
● <T extends Annotation> T getDeclaredAnnotation (Class<T> annotationClass)
该方法用于返回当前元素上直接出现的指定类型的注解,类型由annotationClass指定,如果没有该类型的注解,返回空。
● boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
该方法用于判断当前元素上是否有指定annotationClass类型的注解,如果有返回true,否则返回false。
案例16:建立Calculation类,该类用于银行定期存款利息计算。定义一个成员变量rate,在rate变量前面添加注解Rate,rate的值来自于Rate注解提供的元数据rate。定义一个方法interest(),用于计算本金利息。建立CalculationTest测试类,验证Rate注解的读取。
在annotation包下新建Calculation类。代码如下:
package annotation;
public class Calculation {
// 添加Rate注解
@Rate(rate=0.035)
double dRate;
public Calculation()
{
dRate = 0.0;
}
public double interest(double capital,int year)
{
return capital*dRate*year;
}
public double getdRate() {
return dRate;
}
public void setdRate(double dRate) {
this.dRate = dRate;
}
}
注解的使用和类的使用一样,如果注解类和使用注解的类不在同一个包内,需要使用import语句将注解类导入。Rate注解添加到Calculation类dRate成员变量上,也可以同时添加到其它变量上。
在annotation包下新建CalculationTest类。代码如下:
package annotation;
import java.lang.reflect.Field;
public class CalculationTest {
public static void main(String[] args) {
// 实例化Calculation类
Calculation calcula = new Calculation();
// 获取Calculation类的Class对象
Class calculaClass = calcula.getClass();
try {
//通过class.getDeclaredField(name)获取指定名称的成员变量
Field field = calculaClass.getDeclaredField("dRate");
//获取该成员变量上的Rate注解
Rate rate = field.getAnnotation(Rate.class);
// 判断注解是否获取成功
if( null != rate )
{
// 注解对象直接访问rate属性
calcula.setdRate(rate.rate());
System.out.printf("定期存款年利息为:%.2f/%%\n",rate.rate()*100);
System.out.println("15000元存款2年定期存款利息为:" + calcula.interest(15000,2));
}
else
{
System.out.println("获取Rate注解失败");
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
CalculationTest程序首先通过Calculation实例获取Class对象,通过Class对象的getDeclaredField()方法获取Calculation类成员变量dRate的Field对象,在通过Field类的getAnnotation()方法获取Rate注解,返回的注解对象引用赋值给Rate类型的局部变量rate。
在使用rate对象前,需要判断rate对象是否为空。若不为空,进入利息计算语句块,注解访问属性和类访问属性有一定的差别,注解通过对象名.属性()来访问注解的属性,在属性名称的后面要紧跟一对“()”.
程序执行结果如下图所示:

从执行结果来看,Rate注解获取失败。这是什么原因呢?这是因为在定义Rate注解时,没有给Rate注解添加注解作用范围,因此在程序运行过程就取不到Rate注解了。给注解添加作用范围,就需要涉及到元注解的概念。
元注解
元注解在JDK中已被定义,元注解主要用于对自定义的注解进行描述,也可以理解为注解的注解。
JDK定义了四种元注解,用来对注解的位置、作用范围、文档抽取、继承进行描述。这四种元注解分别是:@Target、@Retention、@Documented、@Inherited,下面分别对这四种元注解进行说明。
@Target
用于描述注解出现的位置,注解可以用于包、类、接口、类成员方法、类成员变量、方法参数、局部变量、枚举类型,JDK定义了ElementType枚举类型用于描述注解可以出现的位置,ElementType有如下枚举值:
ElementType.FIELD:注解仅用于添加到成员变量
ElementType.METHOD:注解仅用于添加到成员方法
ElementType.PARAMETER:注解仅用于添加到方法参数
ElementType.CONSTRUCTOR:注解仅用于添加到构造方法
ElementType.LOCAL_VARIABLE:注解仅用于添加到局部变量
ElementType.ANNOTATION_TYPE:注解仅用于添加到注解(元注解)
ElementType.PACKAGE:注解仅用于添加到包
描述注解可以使用多个枚举值。例如:如果希望注解添加到成员方法和成员变量上,可以使用ElementType.FIELD和ElementType.METHOD枚举值来描述注解。
示例:
@Target(ElementType.METHOD, ElementType.FIELD )
public @interface Demo {
......
}
@Retention
用于描述注解的作用范围,即注解的有效范围。JDK定义了三种有效范围,分别是:注解作用于源代码(编译器可读取注解)、注解作用于类文件(在类文件中可读取注解)、注解作用于运行过程(用反射技术读取注解)。JDK定义了RetentionPolicy枚举类型用于描述注解作用范围,RetentionPolicy有如下枚举值:
RetentionPolicy.SOURCE:注解作用于源代码
RetentionPolicy.CLASS:注解作用于类文件
RetentionPolicy.RUNTIME:注解作用于运行时
示例:
@ Retention (RetentionPolicy.RUNTIME )
public @interface Demo {
......
}
当注解作用范围选择RetentionPolicy.RUNTIME时,作用范围自然就满足类文件和源代码。
@Documented
使用@Documented描述的注解可以被javadoc工具抽取作为程序文档。它是一个标记注解,没有属性。
示例:
@ Documented
public @interface Demo {
......
}
@Inherited
使用@Inherited描述的注解具有继承性,使用@Inherited注解的类,其子类将自动拥有该注解。它是一个标记注解,没有属性。
学习了元注解后,就可以解决前面一课案例读取Rate注解失败的问题,需要在Rate注解前面添加@ Retention (RetentionPolicy.RUNTIME )元注解。
为了不影响前面一课案例的源代码,本课重新编写案例代码。
案例17:建立NewRate注解,NewRate注解为其它类提供银行定期存款利率元数据。建立NewCalculation类,该类用于银行定期存款利息计算。定义一个成员变量rate,在rate变量前面添加注解NewRate,rate的值来自于NewRate注解提供的元数据rate。定义一个方法interest(),用于计算本金利息。建立NewCalculationTest测试类,验证NewRate注解的读取。
在annotation包下新建NewRate注解类。代码如下:
package annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
//注解存在于运行时
@Retention(RetentionPolicy.RUNTIME)
public @interface NewRate {
// 利率默认值0.03
double rate() default 0.03;
}
在NewRate注解前面添加元注解@Retention,元注解的值选择RetentionPolicy.RUNTIME,即注解作用范围为运行时。@Retention元注解和RetentionPolicy枚举类型都在java.lang.annotation包内,需要使用import语句导入。
在annotation包下新建NewCalculation类。代码如下:
package annotation;
public class NewCalculation {
// 添加Rate注解
@NewRate(rate = 0.035)
double dRate;
public NewCalculation()
{
dRate = 0.0;
}
public double interest(double capital, int year) {
return capital * dRate * year;
}
public double getdRate() {
return dRate;
}
public void setdRate(double dRate) {
this.dRate = dRate;
}
}
如果注解类和使用注解的类不在同一个包内,需要使用import语句将注解类导入。NewRate注解添加到NewCalculation类dRate成员变量上,也可以同时添加到其它变量上。
在annotation包下新建NewCalculationTest测试类。代码如下:
package annotation;
import java.lang.reflect.Field;
public class NewCalculationTest {
public static void main(String[] args) {
// 实例化Calculation类
NewCalculation calcula = new NewCalculation();
// 获取Calculation类的Class对象
Class calculaClass = calcula.getClass();
try {
// 通过class.getDeclaredField(name)获取指定名称的成员变量
Field field = calculaClass.getDeclaredField("dRate");
// 获取该成员变量上的Rate注解
NewRate rate = field.getAnnotation(NewRate.class);
// 判断注解是否获取成功
if (null != rate) {
// 注解对象直接访问rate属性
calcula.setdRate(rate.rate());
System.out.printf("定期存款年利息为:%.2f/%%\n", rate.rate() * 100);
System.out.println("15000元存款2年定期存款利息为:" + calcula.interest(15000, 2));
} else {
System.out.println("获取Rate注解失败");
}
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
NewCalculationTest程序首先通过NewCalculation实例获取Class对象,通过Class对象的getDeclaredField()方法获取NewCalculation类成员变量dRate的Field对象,在通过Field类的getAnnotation()方法获取NewRate注解,返回的注解对象引用赋值给NewRate类型的局部变量rate。
程序执行结果如下图所示:

从程序执行结果可以看出,NewCalculation类的NewRate注解的属性被成功读取,并设置到calcula对象的dRate成员变量。
内置的注解
Java内置的注解主要给编译器提供类的编译信息,主要有@Override、@Deprecated、@SuppressWarnings注解,这三个注解java程序员应该经常见到,下面对这三个注解分别进行说明。
@Override
这是一个标记注解,注解的元数据就是自身。该注解的出现位置是在类成员方法前面,通知编译器使用该注解的方法重写了父类的方法。在编译过程中,当编译器检测到某个方法添加了“@Override”注解时,编译器就会检查当前方法在父类是否存在,如果父类没有该方法,编译器就会给出警告信息。
@Deprecated
这也是一个标记注解,用来通知编译器使用该注解的类、方法、属性已经过时(废弃不用),编译器会显示警告信息,提醒开发者正在使用一个过时的类、方法或属性。
@SuppressWarnings
该注解用来阻止编译器发出警告信息。编译器在编译代码时,会输出一些安全警告信息,但会忽略@SuppressWarnings注解标记的元素产生的警告信息。
该注解可用于标记类、成员变量、方法、参数和局部变量,注解的作用范围为源代码级,注解信息不会保留在class文件中,注解的属性为value,是String类型的数组,可以忽略多种类型的警告信息。