Java注解(Annotation)、元注解、自定义注解

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

自定义注解通常都会指明两个元注解:@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

案例分析

  1. 模拟Junit测试的注解@Test,首先需要编写自定义注解@MyTest,并添加元注解,保证自定义注解只能修饰方法,且在运行时可以获得。
  2. 然后编写目标类,给目标方法使用 @MyTest注解,编写三个方法,其中两个加上@MyTest注解。
  3. 最后编写调用类,使用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);
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值