夯实Java基础(十七)——注解(Annotation)

1、注解概述

从JDK5.0开始,Java增加对元数据(MetaData)的支持,也就是注解(Annotation)。其实我们早就已经接触过注解了,例如我们经常在Java代码中可以看到 “@Override”,“@Test”等等这样的东西,它们就是Java中的注解。注解可以像修饰符一样使用,可以用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明。

我们需要注意的是,注解与注释是有一定区别的,注解就是代码里面的特殊标记,这些标记可以在编译,类加载,运行时被读取,并执行相应的处理。通过注解开发人员可以在不改变原有代码和逻辑的情况下在源代码中嵌入补充信息。而注释则是用以说明某段代码的作用,或者说明某个类的用途、某个方法的功能和介绍,以及该方法的参数和返回值的数据类型及意义等等。

2、Java内置注解

在JavaSE部分,注解的使用往往比较简单,Java中提供了5个内置注解,它们分别是:

①、@Override:标注该方法是重写父类中的方法。

这个注解一个是我们见得最多的一个了,提示这个方法是重写于父类的方法。

《夯实Java基础(十七)——注解(Annotation)》

②、@Deprecated:标记某个功能已经过时,用于定义过时的类、方法、成员变量等。

这个注解想必大家应该都有碰到过,在使用Date日期类的时候,里面有大量过时的方法,我们来定义一个Date类来调用一个方法。

《夯实Java基础(十七)——注解(Annotation)》

这个getDay()方法就是过时的,我们点击进去看一下这个方法的源码:

《夯实Java基础(十七)——注解(Annotation)》

果然这个方法是用@Deprecated修饰过的。同时也可以发现我们在调用过时元素时,编译器在编译阶段遇到这个注解时会发出提醒警告,告诉开发者正在调用一个过时的元素,当然如果不想看到警告我们可以抑制它的出现。

③、@SuppressWarnings:抑制编译器警告。

上面说到用@Deprecated修饰过的元素在调用时会有警告,我们可以用@SuppressWarnings注解来抑制警告的出现。

《夯实Java基础(十七)——注解(Annotation)》

可以发现左边的警告没有了。@SuppressWarnings这个注解中参数非常的多,这里介绍几个常见的参数:

  • all:抑制所有警告。
  • deprecation:抑制过期方法警告。
  • null:忽略对null的操作。
  • unchecked:抑制没有进行类型检查操作的警告。
  • unused:抑制没被使用过的代码的警告。

如果需要了解更多的可以去查看官方文档。

④、@FunctionaInterface:指定接口必须为函数式接口。

这个注解是Java8出现的新特性。这个函数式接口的意思就是接口中有一个且仅有一个抽象方法,但是可以有多个非抽象方法,如果不定义或定义多个抽象方法就会报错。

《夯实Java基础(十七)——注解(Annotation)》

正式因为JDK 8中lambda表达式的引入,使得函数式接口在Java中变得越来越流行。因为这些特殊类型的接口可以用lambda表达式、方法引用或构造函数引用轻松替换。

⑤、@SafeVarargs:抑制”堆污染警告”。

这个注解是在Java7中引入,主要目的是处理可变长参数中的泛型,此注解告诉编译器:在可变长参数中的泛型是类型安全的。可变长参数是使用数组存储的,而数组和泛型不能很好的混合使用。因为数组元素的数据类型在编译和运行时都是确定的,而泛型的数据类型只有在运行时才能确定下来,因此当把一个泛型存储到数组中时,编译器在编译阶段无法检查数据类型是否匹配,因此会给出警告信息。

我们来看下面这个示例:

public class Test {
    @SafeVarargs//这里告诉编译器类型安全,不让有警告。其实方法体内容类型不安全
    public static void show(List<String>...lists){
        Object[] arry=lists;
        List<Integer> intList=Arrays.asList(11,22,33);
        arry[0]=intList;//这里就是堆污染,这里没有警告,是因为只针对于可变长参数泛型
        String str=lists[0].get(0);//java.lang.ClassCastException
    }

    public static void main(String[] args) {
        List<String> list1=Arrays.asList("AA","BB","CC");
        List<String> list2=Arrays.asList("DD","EE","DD");
        show(list1,list2);
    }
}

通过上述的示例,我们将intList赋给array[0],array[0]的类型是List<String>,但是储引用到实际为List<Integer>类型的值,这个无效的引用被称为堆污染。由于直到运行时才能确定此错误,因此它会在编译时显示为警告,这里没有警告,是因为只针对于可变长参数泛型,并在运行时出现ClassCastException。

注意:@SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。

3、自定义注解

我们在享受注解给我们带来方便地同时,我们自己应该要知道怎么去定义注解。注解的自定义非常的简单,通过 @interface关键字进行定义,可以发现这个关键字和接口interface很相似,就在前面加了一个 @符号,但是它和接口没有任何关系。自定义注解还需要注意的一点是:所有的自定义注解都自动继承了java.lang.annotation.Annotation这个接口。自定义注解的格式:

public @interface 注解名 {
       //属性
}    

同样我们可以在注解中定义属性,它的定义有点类似于方法,但又不是方法,在注解中是不能声明普通方法的。注解的属性在注解定义中以无参数方法的形式来声明,其方法名定义了属性的名字,其返回值定义了该属性的类型,我们称为配置参数。它们的类型只能是八种基本数据类型、String类型、Class类型、enum类型、Annotation类型以上所有类型的数组。例如:

//定义了一个MyAnnotation注解
public @interface MyAnnotation {
    String[] value();
}

@MyAnnotation(value = "hello")
class Test{

}

上面注解代码中,定义了一个String的value数组。然后我们在使用的时候,就可以使用 属性名称=“xxx” 的形式赋值。

注解中属性还可以有默认值,默认值需要用 default 关键值指定。比如:.

//定义了一个MyAnnotation注解
public @interface MyAnnotation {
    String id();
    String[] value() default {"AA","BB"};
}

@MyAnnotation(id="one")
class Test{

}

上面定义了 id 属性没有默认值,而value属性中则设置了默认值,所以在使用注解的时候只需给 id 属性赋值即可,value可以不用写。

通过以上形式自定义的注解暂时都还没有任何实用的价值,因为自定义注解必须配上注解的信息处理流程(使用反射)才有意义。如何让注解真真的发挥作用,主要就在于注解处理方法,所以接下来我们将学习元注解和注解的反射。

4、元注解

元注解就是用来修饰其他注解的注解。我们随便点进一个注解的源码都可以发现有元注解。

《夯实Java基础(十七)——注解(Annotation)》

Java5.0中定义了4个标准的元注解类型,它们被用来提供对其它注解类型作说明:

  • @Retention
  • @Target
  • @Documented
  • @Inherited

而Java8.0中又增加了一个新的元注解类型:

  • @Repeatable

所以接下来我们将逐个分析它们的作用和使用方法。

1、@Retention:用于指定该Annotation的生命周期。

这个元注解只能用于修饰一个Annotation定义,它的内部包含了一个RetentionPolicy枚举类型的属性,而这个枚举类中定义了三个枚举实例,SOURCE、CLASS、RUNTIME。它们各个值的意思如下:

  • RetentionPolicy.SOURCE:在源文件中有效(即源文件保留),在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS:在class文件中有效(即class保留),当Java程序运行时,它并不会被加载到 JVM 中,只保留在class文件中。这个是默认值。
  • RetentionPolicy.RUNTIME:在运行时有效(即运行时保留),当Java程序运行时,注解会被加载进入到 JVM 中,所以我们可以使用反射获取到它们。

 《夯实Java基础(十七)——注解(Annotation)》

比较典型的是@SuppressWarnings注解,如果我们用 javap -c去反编译它是看到这个注解的,因为在编译的时候就已经被丢弃了。

②、@Target:用于指定该Annotation能够用在哪些地方。

@Target内部定义了一个枚举类型的数组ElementType[] value(),在ElementType这个枚举类中参数有很多,我们来看一下:

  • TYPE:用于描述类、接口(包括注解类型) 或enum声明
  • FIELD:用于描述域即类成员变量
  • METHOD:用于描述方法
  • PARAMETER:用于描述参数
  • CONSTRUCTOR:用于描述构造器
  • LOCAL_VARIABLE:用于描述局部变量
  • ANNOTATION_TYPE:由于描述注解类型
  • PACKAGE:用于描述包
  • TYPE_PARAMETER:1.8版本开始,描述类、接口或enum参数的声明
  • TYPE_USE:1.8版本开始,描述一种类、接口或enum的使用声明

③、@Document:表示Annotation可以被包含到javadoc中去。默认情况下javadoc是不包含注解的。

由于这个比较简单所以不细说。

④、@Inherited:被它修饰的Annotation将具有继承性。

@Inherited修饰过的Annotation其子类会自动具有该注解。在实际应用中,使用情况非常少。

⑤、@Repeatable:用于指示它修饰的注解类型是可重复的。

这个注解是在Java8中新出的特性,说到这个可重复注解可能有点不理解。我们通过示例来理解一下:

先定义一个MyAnnotation注解:

@Inherited
@Documented
@Repeatable(MyAnnotations.class)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})
public @interface MyAnnotation {
    String value() default "Hello";
}

这里需要说明@Repeatable(MyAnnotations.class),它表示在同一个类中@MyAnnotation注解是可以重复使用的,重复的注解被存放至@MyAnnotations注解中。

然后再定义一个MyAnnotations注解:

@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD,ElementType.PARAMETER})
public @interface MyAnnotations {
    MyAnnotation[] value();
}

这个MyAnnotations注解里面的属性必须要声明为要重复使用注解的类型数组MyAnnotation[] value();,而且相应的生命周期和使用地方都需要同步。否则就会编译报错!

进行测试:

@MyAnnotation(value = "World")
@MyAnnotation(value = "World")
public class Test{

}

而在Java8之前没有@Repeatable注解是这样写的:

@MyAnnotations({@MyAnnotation(value = "World"),@MyAnnotation(value = "World")})
public class Test{

}

5、注解处理器(使用反射)

以上讲的所以注解的定义都只是一个形式,实际上还并没有什么可使用的价值,而注解的核心就是使用反射来获取到它们,所以下面我们要来学习注解处理器(使用反射)的使用。

下面参考:https://www.cnblogs.com/peida/archive/2013/04/26/3038503.html

java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射API扩充了读取运行时Annotation信息的能力。当一个Annotation类型被定义为RUNTIME的注解后,该注解才能是运行时可见,当class文件被装载时被保存在class文件中的Annotation才会被虚拟机读取。
AnnotatedElement 接口是所有程序元素(Class、Method和Constructor)的父接口,所以程序通过反射获取了某个类的AnnotatedElement对象之后,程序就可以调用该对象的如下四个个方法来访问Annotation信息:

  • 方法1:<T extends Annotation> T getAnnotation(Class<T> annotationClass): 返回改程序元素上存在的、指定类型的注解,如果该类型注解不存在,则返回null。
  • 方法2:Annotation[] getAnnotations():返回该程序元素上存在的所有注解。
  • 方法3:boolean is AnnotationPresent(Class<?extends Annotation> annotationClass):判断该程序元素上是否包含指定类型的注解,存在则返回true,否则返回false.
  • 方法4:Annotation[] getDeclaredAnnotations():返回直接存在于此元素上的所有注释。与此接口中的其他方法不同,该方法将忽略继承的注释。(如果没有注释直接存在于此元素上,则返回长度为零的一个数组。)该方法的调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响。

一个简单的注解处理器:  

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)//这里必须定义为RUNTIME
public @interface MyAnnotation {
    String id();
    String[] value() default {"AA","BB"};
}

@MyAnnotation(id = "hello")
class Test{
    public static void main(String[] args) {
        boolean annotationPresent = Test.class.isAnnotationPresent(MyAnnotation.class);
        System.out.println(annotationPresent);
        if ( annotationPresent ) {
            MyAnnotation myAnnotation = Test.class.getAnnotation(MyAnnotation.class);

            System.out.println("id:"+myAnnotation.id());
            System.out.println("value:"+ Arrays.toString(myAnnotation.value()));
        }
    }
}

程序运行结果:

《夯实Java基础(十七)——注解(Annotation)》

上面的例子只是作用在类上面的注解,如果要作用在属性、方法等上面的注解我们应该怎么获取呢?

定义作用于类上面的MyAnnotation注解:

//作用于类上面的注解
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)//这里必须定义为RUNTIME
public @interface MyAnnotation {
    String[] value() default "";
}

定义作用于属性上面的AttributeAnnotation注解:

//作用于属性上的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)//这里必须定义为RUNTIME
public @interface AttributeAnnotation {
    String value();
}

定义作用于方法上面的MethodAnnotation注解:

//作用于方法上的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)//这里必须定义为RUNTIME
public @interface MethodAnnotation {
    String value();
}

然后就是测试了:

@MyAnnotation(value = "MyAnnotation")
class Test{

    @AttributeAnnotation(value = "AttributeAnnotation")
    String str;

    @MethodAnnotation(value = "MethodAnnotation_show")
    public void show(){
        System.out.println("MethodAnnotation_show");
    }

    @MethodAnnotation(value = "MethodAnnotation_display")
    public void display(){
        System.out.println("MethodAnnotation_display");
    }

    public static void main(String[] args) {
        //获取类上面的注解
        boolean annotationPresent = Test.class.isAnnotationPresent(MyAnnotation.class);
        System.out.println(annotationPresent);
        if ( annotationPresent ) {
            MyAnnotation myAnnotation = Test.class.getAnnotation(MyAnnotation.class);
            System.out.println("class-annotation:"+Arrays.toString(myAnnotation.value()));
        }

        try {
            //获取单个属性中的注解
            Field str = Test.class.getDeclaredField("str");
            AttributeAnnotation attributeAnnotation = str.getAnnotation(AttributeAnnotation.class);
            if (attributeAnnotation!=null){
                System.out.println("attribute-annotation:"+attributeAnnotation.value());
            }

            //获取多个方法中的注解
            Method[] declaredMethods = Test.class.getDeclaredMethods();
            for (Method declaredMethod : declaredMethods) {
                    MethodAnnotation methodAnnotation = declaredMethod.getAnnotation(MethodAnnotation.class);
                    if (methodAnnotation!=null){
                        System.out.println("method-annotation:"+methodAnnotation.value());
                    }
            }

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }

    }
}

程序运行结果:

《夯实Java基础(十七)——注解(Annotation)》

小弟菜鸟只能领悟这么多了,如果有错误或者需要补充的地方欢迎大家留言指出。谢谢!!!

    原文作者:tang_hao
    原文地址: https://www.cnblogs.com/tang-hao-/p/11340632.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞