目录
1. 注解概述
注解(Annotation)是 JDK5.0增加的功能,该机制允许在Java代码中添加特殊标记,并允许通过反射(reflection),以编程方式访问这些特殊标记。通过使用注解,开发人员可以在不改变原有逻辑的情况下,为程序元素(类、字段、方法等)附加额外的补充信息。代码分析工具、开发工具和部署工具可以通过这些补充信息进行验证和部署。
注解不直接影响程序的语义。然而,开发和部署工具可以读取这些注解,并以某种形式处理这些注解,可能生成其他 Java源程序、XML配置文件或者与包含注解的程序一起使用的其他组件,从而影响程序的语义。注解信息可以从源代码中读取,从编译后的.class文件中读取,也可以通过反射机制在运行时读取。
注解也是一种数据类型,与类、接口、枚举是在同一个层次。
注解,也是一种特殊的注释,它是代码级别的注释,是用代码注释代码。不同于单行注释//,多行注释/* * /
2. 注解的使用
使用注解:@注解名称
- JDK中系统内置了常用的三个基本注解:
- @Override:检查这个方法是否符合重写的要求
- @Deprecated:标记某个类、方法、属性等已过时,不建议程序员继续使用,因为可能有问题,也可能是不好用,有更好的实现
- @SuppressWarnings:抑制编译器警告
public class TestAnnotation {
public static void main(String[] args) {
@SuppressWarnings("unused")
int a = 10;
}
@Deprecated
public void print(){
System.out.println("过时的方法");
}
@Override
public String toString() {
return "重写的toString方法()";
}
}
3. 元注解(meta-annotation)
元注解:对现有的注解进行解释说明的注解。JDK5.0定义了4个标准的meta-annotation:
@Target、@Retention、@Documented、@Inherited
@Target
- 作用:指明被修饰的注解能用于修饰哪些程序元素,如果不写默认是任何地方都可以使用
- 可选的参数值定义在在
java.lang.annotation
包的枚举类型ElemenetType中:
- 可选的参数值定义在在
TYPE:用在类、接口、注解、枚举上
FIELD:用在成员变量(包括枚举常量)上
METHOD:用在方法上
PARAMETER:用在方法参数上
CONSTRUCTOR:用在构造方法上
LOCAL_VARIABLE:用在局部变量上
ANNOTATION_TYPE:用在注解类型上
PACKAGE:用在包上
例如,从@Override注解的源码可以知道@Override只能用来修饰方法
@Target(ElementType.METHOD) //只能用来修饰方法
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Retention
- 作用:指明被修饰的注解可以保留到什么阶段,即注解的生命周期(有效范围),如果不写默认是CLASS
- 可选的参数值定义在
java.lang.annotation
包的枚举类型RetentionPolicy(保留策略)中:
- 可选的参数值定义在
SOURCE:注解只存在于Java源代码中,编译生成的字节码文件中没有
CLASS:注解存在于Java源代码、编译生成的.class字节码文件中,运行的时候内存中没有。不写Retention注解,默认就是CLASS
RUNTIME:注解存在于Java源代码中、编译生成的.class字节码文件中、运行时内存中,程序可以通过反射获取该注解。
例如,从@Override注解的源码可以知道@Override只存在于源代码中,用来检查被修饰的方法是否符合重写的要求。
@Target(ElementType.METHOD) //只能用来修饰方法
@Retention(RetentionPolicy.SOURCE) //@Override只存在于源代码中
public @interface Override {
}
只有声明为RUNTIME的注解,才能通过反射获取。
@Documented
- 作用:指明被修饰的注解在被javadoc解析时应该被保留下来提取到API帮助文档中
@Inherited
- 作用:指明被修饰的注解用在类上时自动被子类继承
4. 自定义注解
定义格式
使用 @interface 定义注解:
//声明注解
[修饰符] @interface 注解名称{
配置参数列表
}
注解也是定义在 .java 文件中的,它也会被编译成 .class文件。
定义注解和定义接口相似,注解前面的访问修饰符和接口一样有两种,分别是公有访问权限(public)和默认访问权限(默认不写)。一个源程序文件中可以声明多个注解,但只能有一个是公有访问权限的注解,且源程序文件名和公有访问权限的注解名一致。
注解类型本质上就是一个继承 java.lang.annotation.Annotation
接口的接口,所有的注解类型都默认继承 Annotation
接口,其具体实现类是Java运行时生成的动态代理类。
但是:Annotation
接口本身不用于定义注解类型,一个接口手动继承 Annotation
接口也不会解析为注解类型。通过 @interface 定义注解后,该注解不能继承其他的注解或接口。
注解的配置参数
-
参数的定义
- 注解中可以定义成员变量,成员变量以无参数方法的形式来声明,方法名和返回值定义了该成员的名字和类型,称为配置参数
- 配置参数的本质就是抽象方法
-
配置参数的数据类型
- 8种基本数据类型(byte,short,int,long,float,double,char,boolean)
- String类型,Class类型,枚举类型,注解类型
- 以上所有类型的一维数组。
//声明MyAnnotation注解 @interface MyAnnotation { String name(); //String类型的参数name int age(); //int类型的参数age String[] value(); //String[]类型的参数value }
-
配置参数初始值(默认值)
- 可以在定义注解的配置参数时为其指定初始值,指定配置参数的初始值可使用 default 关键字
@interface MyAnnotation { String name() default "java"; //name参数有默认值"java" int age() default 25; //age参数有默认值25 String[] value(); }
-
因此,根据是否有初始值,配置参数有两种声明格式:
- 格式1:数据类型 参数名();
- 格式2:数据类型 参数名() default 默认值;
-
如果为注解的参数指定了默认值,那么使用该注解时就可以不为这些参数赋值,而是直接使用默认值。当然也可以为有默认值的参数指定其他值,如果指定了其他值,则默认值不会起作用。
-
如果参数没有默认值,那么在使用注解时一定要给参数赋值。
-
配置参数的使用
-
如果定义的注解没有配置参数,那么使用时直接写 @注解名 ,例如:@Override
-
如果定义的注解含有配置参数,那么使用时必须指定参数值,除非它有默认值。
- 格式是“参数名 = 参数值”,多个参数之间 , 隔开:@注解名(属性名 = 属性值, 属性名 = 属性值, 属性名 = 属性值…)
//声明MyAnnotation注解 @interface MyAnnotation { String name() default "java"; //name参数有默认值"java" int age(); String[] value(); } //使用注解 修饰类 @MyAnnotation(age = 25, value = {"aaa", "bbb", "ccc"}) class MyClass{ }
-
-
参数是数组类型的使用
- 给数组类型的参数赋值时,如果有多个元素,大括号{}不能省略;如果只有一个元素,大括号{}可以省略
//声明MyAnnotation注解 @interface MyAnnotation { String name() default "java"; //name参数有默认值"java" int age(); String[] value(); } //使用注解 修饰类 @MyAnnotation(age = 25, value = {"aaa", "bbb", "ccc"}) //数组类型参数value有多个元素,大括号{}不能省略 class MyClass{ //使用注解 修饰方法 @MyAnnotation(name = "C语言", age = 38, value = "abc") //数组类型参数value只有一个元素,大括号{}可以省略 public void method() { } }
-
特殊参数 value
- 如果注解中只有一个参数成员且名称为 value,在使用注解时,给 value 参数赋值可以省略“value = ” ,直接给属性值,无论value是单值元素还是数组类型
- 如果注解中只有一个参数成员,建议使用 value 作为参数名
- 如果除了 value 参数的其他参数都有默认值,使用注解给 value 参数赋值时也可以省略”value = “;如果给其他参数赋新值不使用默认值,不能省略”value = “
//声明MyAnno注解,只有一个参数value @interface MyAnno { String value(); } //声明MyAnnotation注解 @interface MyAnnotation { String name() default "java"; //name参数有默认值"java" int age() default 25; //age参数有默认值25 String[] value(); } //使用注解 修饰类 @MyAnno("AAAA") //等价于@MyAnno(value = "AAAA") 注解中只有一个参数value,给value参数赋值时可以省略value = @MyAnnotation({"aaa", "bbb", "ccc"}) //如果没有覆盖name、age参数的默认值,给value参数赋值时可以省略value = class MyClass{ }
- 如果注解中除了value参数还有其他参数,且至少有一个参数没有默认值,则在使用注解给value参数赋值时,“value = ” 不能省略
//声明MyAnnotation注解 @interface MyAnnotation { String name() default "java"; //name参数有默认值"java" int age(); String[] value(); } //使用注解 修饰类 @MyAnnotation(age = 25, value = {"aaa", "bbb", "ccc"}) //age参数没有默认值,给value参数赋值时不能省略value = class MyClass{ }
- 如果注解中只有一个参数成员且名称为 value,在使用注解时,给 value 参数赋值可以省略“value = ” ,直接给属性值,无论value是单值元素还是数组类型
自定义注解通常都会指明两个元注解:@Target、@Retention
自定义注解必须配上注解的信息处理流程(使用反射)才有意义。
5. 注解解析
通过Java技术获取注解数据的过程则称为注解解析。
与注解解析相关的接口
-
java.lang.annotation.Annotation
:所有注解类型继承的公共接口 -
java.lang.reflect.AnnotatedElement
:定义了与注解解析相关的方法,常用方法以下四个:boolean isAnnotationPresent(Class annotationClass):判断此元素是否有指定类型的注解,有则返回true,否则返回false T getAnnotation(Class<T> annotationClass):获取此元素上指定类型的注解对象,不存在指定类型的注解则返回null Annotation[] getAnnotations():获取此元素上及其从父类上继承的所有的注解对象。 Annotation[] getDeclaredAnnotations():获得直接存在于此元素上所有的注解对象,不包括父类的。
获取注解数据的原理
注解作用在哪个成员上,就通过反射获得该成员的对象来得到它的注解。
-
如注解作用在类上,就通过Class对象得到它的注解。
// 获得Class对象 Class c = 类名.class; // 根据注解的Class获得使用在类上的注解对象 注解类型 变量名 = c.getAnnotation(注解类型.class);
-
如注解作用在方法上,就通过方法(Method)对象得到它的注解。
// 得到方法对象 Method method = clazz.getDeclaredMethod("方法名"); // 根据注解名得到方法上的注解对象 注解类型 变量名 = method.getAnnotation(注解类型.class);
使用反射获取注解的数据
代码实现
声明注解MyAnnotation,为了能在运行期获取该注解,需要将 @Retention 的参数设置为RetentionPolicy.RUNTIME
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
String name() default "java";
int age();
String[] value();
}
MyClass类,使用 @MyAnnotation 注解修饰:
@MyAnnotation(age = 25, value = {"aaa", "bbb", "ccc"})
class MyClass{
}
调用类:
public class MyAnnotationTest {
public static void main(String[] args) {
// 获取MyClass类对应的Class对象
Class clazz = MyClass.class;
// 判断MyClass类是否使用了MyAnnotation注解
if (clazz.isAnnotationPresent(MyAnnotation.class)) {
// 根据注解类型的Class对象获取注解类型对象
MyAnnotation ma = (MyAnnotation) clazz.getAnnotation(MyAnnotation.class);
// 获取MyAnnotation注解的配置参数
System.out.println("name:" + ma.name());
System.out.println("age:" + ma.age());
System.out.println("value:" + Arrays.toString(ma.value()));
}
}
}
6. 模拟Junit
案例分析
- 模拟Junit测试的注解@Test,首先需要编写自定义注解@MyTest,并添加元注解,保证自定义注解只能修饰方法,且在运行时可以获得。
- 然后编写目标类,给目标方法使用 @MyTest注解,编写三个方法,其中两个加上@MyTest注解。
- 最后编写调用类,使用main方法调用目标类,模拟Junit的运行,只要有@MyTest修饰的方法都会运行。
注解MyTest
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyTest {
}
目标类MyTestClass
class MyTestClass {
@MyTest
public void test01(){
System.out.println("test01");
}
public void test02(){
System.out.println("test02");
}
@MyTest
public void test03(){
System.out.println("test03");
}
}
调用类MyTestDemo
public class MyTestDemo {
public static void main(String[] args) throws Exception{
// 获取MyTestClass类Class对象
Class c = MyTestClass.class;
// 获得所有的成员方法对象
Method[] methods = c.getMethods();
// 创建MyTestClass类对象
Object obj = c.newInstance();
// 遍历数组
for (int i = 0; i <methods.length; i++) {
Method m = methods[i];
// 判断方法m上是否使用注解MyTest
if(m.isAnnotationPresent(MyTest.class)){
// 执行方法m
m.invoke(obj);
}
}
}
}