Java注解Annotation从入门到精通

Java注解入门

  • 一、注解是什么

注解(Annotation)是Java代码里的特殊标记,比如:@Override、@Test等,其作用是:通过反射机制让其他程序根据注解信息来决定怎么执行该程序。具体的讲有三种作用:

  1. 编译器信息——编译器可以使用注解来检测错误或抑制警告。
  2. 编译时和部署时处理——软件工具可以处理注解信息以生成代码、XML文件等。
  3. 运行时处理——有些注解可以在运行时进行检查。

注解可以用在类上、构造器上、方法上、成员变量上、参数上等位置。

  • 二、JDK中预定义的注解类型

Java SE API中预先定义了一组注释类型。一些注解类型由Java编译器使用,而另一些则适用于注解其他注解(你没看错)。

(一)Java语言使用的注解类型

在java.lang包中预定义的注解类型有@Deprecated、@Override和SuppressWarnings。

@Deprecated注解

 @Deprecated 注释表示标记的元素已弃用,不应再使用。每当程序使用带有@Deprecated注释的方法、类或字段时,编译器都会生成警告。当元素被弃用时,还应使用Javadoc @Deprecated标记进行记录,如下例所示。在Javadoc注释和注释中使用at符号(@)并非巧合:它们在概念上是相关的。此外,请注意,Javadoc 标记以小写d开始,注解以大写D开始。

// Javadoc comment follows

    /**

     * @deprecated

     * explanation of why it was deprecated

     */

    @Deprecated

    static void deprecatedMethod() { }

}
@Override注解

@Override注释通知编译器该元素旨在覆盖超类中声明的元素。虽然在重写方法时不强制使用此注释,但它有助于防止错误。如果一个标记为@Override的方法未能正确重写其超类中的一个方法,编译器将报错。

// mark method as a superclass method

   // that has been overridden

   @Override

   int overriddenMethod() { }
@SuppressWarnings注解

@SuppressWarnings注解告诉编译器抑制在未加注本注解时会生成的特定警告。在下面的示例中,使用了已弃用的方法,编译器通常会生成警告。然而,在加注本注解后,会导致警告被抑制。

// use a deprecated method and tell

   // compiler not to generate a warning

   @SuppressWarnings("deprecation")

    void useDeprecatedMethod() {

        // deprecation warning

        // - suppressed

        objectOne.deprecatedMethod();

    }

每个编译器警告都属于一个类别。Java语言规范列出了两个类别:弃用和未检查。未检查警告可能发生在与在泛型出现之前编写的遗留代码接口时。要抑制多个类别的警告,请使用以下语法:

@SuppressWarnings({"unchecked", "deprecation"})
@SafeVarargs 注解

@SafeVarargs 注解应用于方法或构造函数时,表明代码不会对其可变参数执行潜在的不安全操作。当使用此注解类型时,与可变参数用法相关的未检查警告将被抑制。

@FunctionalInterface 注解

@FunctionalInterface 注解是在Java SE 8中引入的,用于指示类型声明是按照Java语言规范定义的功能接口。

(二)用于注解其他注解的注解(元注解)

用于注解其他注解的注解,叫做元注解。它们定义在在java.lang.annotation包中,JDK提供了多种元注释类型。

@Retention 注解

@Retention 指定了标记的注解如何存储:

  1. RetentionPolicy.SOURCE——标记的注解仅在源代码级别保留,编译器会忽略它。
  2. RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但被Java虚拟机(JVM)忽略。
  3. RetentionPolicy. Runtime - 标记的注解由JVM保留,因此它可供运行时环境使用。
@Documented 注解

@Documented 注解表示,只要使用指定的注解,这些元素都应该使用Javadoc工具进行记录。

@Target 注解

@Target 注解标记另一个注解,以限制该注解可以应用于哪种Java元素。目标注释将以下元素类型之一指定为其值:

  1. ElementType.ANNOTATION_TYPE 可以应用于注释类型。
  2. ElementType.CONSTRUCTOR 可以应用于构造函数。
  3. ElementType.FIELD 可以应用于字段或属性。
  4. ElementType.LOCAL_VARIABLE 可以应用于局部变量。
  5. ElementType.METHOD 可以应用于方法级注释。
  6. ElementType.PACKAGE 可以应用于包声明。
  7. ElementType.PARAMETER 可以应用于方法的参数。
  8. ElementType.TYPE 可以应用于类的任何元素。
@Inherited 注解

@Inherited 注释表示该注解类型可以从父类继承。(默认情况下不是这样的。)当用户查询该注解类型时,如果该类没有该类型的注解,则查询该类的父类以获取该注解类型。该注解仅适用于类声明。

@Repeatable 注解

@Repeatable 注解在Java SE 8中引入,表示标记的注解可以多次应用于相同的声明或类型使用。有关更多信息,请参阅重复注解。

  • 三、定义自己的注解

自定义注解的格式:

public @interface 注解名称 {

    public 属性类型 属性名称() default 默认值;

}

注意如果注解在定义时只有一个value属性,那么默认在使用这个注解时value属性名可以省略,value是一个特殊的属性名称。

如自定一个名为MyAnno的注解:

public @interface MyAnno {

    public String name() default "无名氏";

    public String gender() default "男";

    public int age();

    public String[] hobby();

}

使用自定义的注解去标记类:

@MyAnno(name="注解类", gender = "女", age = 21, hobby = {"唱歌","跳舞","看电视"})

public class AnnoTest {

    public static void main(String[] args) {

        // TODO

    }

}

  • 四、注解的原理

经过反编译可知,注解是一种继承了Annotation接口的特殊接口。如MyAnno反编译后是如下的代码:Annotation类位于java.lang.annotation包中

public interface MyAnno extends Annotation{

    public abstract String name();

    public abstract String gender();

    public abstract int age();

}
  • 六、元注解(常用的@Target和@Retention详解)

元注解是指修饰注解的注解。Java中常见的有两个,分别是@Target和@Retention。

@Target注解

声明被修饰的注解能在哪些位置使用

@Target(ElementType.TYPE)

  1. TYPE:类、接口
  2. FIELD:成员变量
  3. METHOD:成员方法
  4. PARAMETER:方法参数
  5. CONSTRUCTOR:构造方法
  6. LOCAL_VARIABLE:局部变量

如标记注解可以用在类和方法上:@Target({ElementType.TYPE, ElementType.METHOD})

@Retention

声明注解的保留周期。

@Retention(RetentionPolicy.RUNTIME)

  1. SOURCE 只作用在源码阶段,字节码文件中不存在
  2. CLASS(默认值) 保留到字节码文件阶段,运行阶段不存在
  3. RUNTIME(开发常用) 一直保留到运行阶段

如控制注解一直保留到运行时:@Retention(RetentionPolicy.RUNTIME)

  • 七、解析注解

(一)什么是注解的解析?

就是判断类上、方法上、成员变量上是否存在注解,并把注解里的内容给解析出来。

(二)如何解析注解?

指导思想:要解析谁上面的注解,就应该先拿到谁(对象)。

比如要解析类上面的注解,则应该先获取该类的Class对象,再通过Class对象解析其上面的注解。比如要解析成员方法上的注解,则应该获取到该成员方法的Method对象,再通过Method对象解析其上面的注解。

Class、Method、Field、Constructor都实现了AnnotatedElement接口,它们都拥有解析注解的能力。

AnnotatedElement接口提供了解析注解的方法

说明

public Annotation getDeclaredAnnotations()

获取当前对象上的注解

public T getDeclaredAnnotation(Class annotationClass)

获取指定的注解对象

public boolean isAnnotationPresent(Class annotationClass)

判断当前对象上是否存在某个注解

(三)解析注解的案例

需求如下:

①定义注解MyTest4,要求如

  1. 包含属性:String value()
  2. 包含属性:double aaa(),默认值为100>
  3. 包含属性:String[]bbb()
  4. 限制注解使用的位置:类和成员方法上
  5. 指定注解的有效范围:一直到运行时

②定义一个类叫:Demo,在类中定义一个test1方法,并在该类和其方法上使用MyTest4注解

③定义AnnotationTest3测试类,解析Demo类中的全部注解。

定义MyTest4注解:

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;



@Target({ElementType.TYPE, ElementType.METHOD}) // 限制本注解使用在类和成员方法上

@Retention(RetentionPolicy.RUNTIME) // 指定注解的有效范围:一直到运行时

public @interface MyTest4 {

    String value(); // 属性

    double aaa() default 100;   // 属性,默认值100

    String[] bbb();     //属性

}

定义使用自定义注解的类Demo类:

/**

 * 本类用于使用自定义的注解MyTest4

 */

@MyTest4(value = "值1", aaa = 234.5, bbb = {"abc","def","ghi"})

public class Demo {

    @MyTest4(value = "值2", aaa = 123.4, bbb = {"aaa","bbb"})

    public void test1(){

    }

}

使用AnnotationTest3类获取加注在Demo类上的MyTest4注解的具体内容:

import java.lang.reflect.Method;

import java.util.Arrays;

/**

 * 本类解析Demo类中的全部注解

 */

public class AnnotationTest3 {

    public static void main(String[] args) throws Exception {

        // 1.先得到Class对象

        Class c = Demo.class;

        // 2.解析类上的注解

        // 判断类上是否包含了某个注解

        if(c.isAnnotationPresent(MyTest4.class)){ // 使用isAnnotationPresent方法判断是否包含MyTest4注解

            MyTest4 myTest4 = c.getDeclaredAnnotation(MyTest4.class);   // 获取MyTest4对象

            // 打印加在Demo类上的注解的内容

            System.out.println(myTest4.value());

            System.out.println(myTest4.aaa());

            System.out.println(Arrays.toString(myTest4.bbb()));

        }

        System.out.println("-------------------");

        // 3.解析方法上的注解

        Method m = c.getDeclaredMethod("test1");

        if(m.isAnnotationPresent(MyTest4.class)){   // 判断方法上是否有注解

            MyTest4 myTest4 = m.getDeclaredAnnotation(MyTest4.class);

            System.out.println(myTest4.value());

            System.out.println(myTest4.aaa());

            System.out.println(Arrays.toString(myTest4.bbb()));

        }

    }

}

// 输出结果为:

//  值1

//  234.5

//  [abc, def, ghi]

//  -------------------

//  值2

//  123.4

//  [aaa, bbb]

八、注解的应用场景演示

模拟JUnit框架,运行加注注解的方法,实现步骤:定义若干个方法,只要加注了MyTestAnno注解,才会触发该方法执行。

需求

●定义若干个方法,只要加了MyTestAnno注解,就会触发该方法执行。

分析

①定义一个自定义注解MyTestAnno,只能注解方法,存活范围是一直都在。

②定义若干个方法,部分方法加上@MyTestAnno注解修饰,部分方法不加。

③模拟一个junit程序,可以触发加了@MyTestAnno注解的方法执行。

第一步,定义MyTestAnno注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD) // 确定注解的使用范围
@Retention(RetentionPolicy.RUNTIME) // 确定注解的存活范围
// 定义注解
public @interface MyTestAnno {
}

第二步,编写使用MyTestAnno注解的类,该类有多个方法,有部分方法加注了MyTestAnno注解:

/**
 * 本类用于模拟业务类,加注MyTestAnno注解的方法
 * 会执行测试用例
 * 在method2和method5上加注了注解
 */
public class MyUserfulClass {
    public void method1(){
        System.out.println("方法1");
    }
    @MyTestAnno
    public void method2(){
        System.out.println("方法2");
    }
    public void method3(){
        System.out.println("方法3");
    }
    public void method4(){
        System.out.println("方法4");
    }
    @MyTestAnno
    public void method5(){
        System.out.println("方法5");
    }
}

第三步,定义MyJUnitSimulator类,用于模拟JUnit测试用例类,用于遍历MyUsefulClass中加注了MyTestAnno注解的方法:

import java.lang.reflect.Method;

public class MyJUnitSimulator {
    public static void main(String[] args) throws Exception {
        // 定义一个对象用于反射时调用invoke的触发对象
        MyUserfulClass mu = new MyUserfulClass();
        // 取得MyUserfualClass对象
        Class mufc = MyUserfulClass.class;
        // 遍历MyUserfualClass对象中的所有方法
        for (Method m : mufc.getDeclaredMethods()) {
            if(m.isAnnotationPresent(MyTestAnno.class)){    // 判断是否加注了MyTestAnno注释
                m.invoke(mu);   // 如果加注了MyTestAnno注释,则执行该方法
            }
        }
    }
}
// 运行输出为:
// 方法2
// 方法5

   九、可重复注解

在某些情况下,您希望将相同的注解应用于声明或类型使用。自 Java SE 8 版本以来,重复注解使您能够做到这一点。

例如,您正在编写代码以使用定时器服务,该服务使您能够在给定时间或特定时间运行方法,类似于UNIX cron服务。现在您想设置一个定时器,以便在每月最后一天和每个星期五晚上11点运行方法doPeriodicCleanup。要设置定时器运行,需要创建一个@Schedule注解并将其两次应用于doPeriodicCleanup方法。第一次使用指定每月最后一天,第二次指定星期五晚上11点,如下面的代码示例所示:

@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

上一个例子将注解应用于方法。您可以在使用任何标准注解的地方进行重复注解。例如,您有一个用于处理未经授权访问异常的类。您为经理或其他管理员使用一个@Alert注解:

@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }

出于兼容性原因,重复注解存储在Java编译器自动生成的容器注解中。为了使编译器能够执行此操作,代码中需要两个声明。

(一)定义一个重复注解

步骤一 声明一个重复注解类型

自定义的注解类型必须标记有@Repeatable元注解。以下示例定义了自定义的@Schedule可重复注解类型:

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

元注解@Repeatable括号中的值(本例中为:Schedules.class)是Java编译器生成的、用于存储重复注解的容器注解(container annotation的类型。 在此示例中,容器注解类型是Schedules,也即重复的@Schedule注解存储在@Schedules注解中。

在没有首先声明一个注解是可重复的情况下,将该注解重复应用于声明会导致编译时错误。

步骤二 声明容器注解类型(Containing Annotation Type)

容器注解类型必须具有一个数组类型的值元素。可重复的数组类型必须是要包含的重复注释类型。容器注解类型的Schedules的声明如下:

public @interface Schedules {
    Schedule[] value();
}

(二)检索注解

反射API中有几种方法可用于检索注释。返回单个注解的方法的行为(如 AnnotatedElement.getAnnotation(Class))没有改变,因为它们只返回单个注释,前提是存在所请求类型的注释。如果存在多个所请求类型的注解,则可以通过首先获取其容器注解来获取它们。通过这种方式,遗留代码可以继续工作。Java SE 8 中引入了其他方法,这些方法可以扫描容器注解或一次返回多个注解,例如 AnnotatedElement.getAnnotationsByType(Class)。有关所有可用方法的信息,请参阅 AnnotatedElement 类规范。

(三)设计时的考虑因素


在设计注解类型时,您必须考虑该类型注解的基数。现在,可以使用零次、一次或多次注解(如果注解的类型标记为@Repeatable)。还可以通过使用@Target元注解来限制注解类型的使用位置。例如,您可以创建一个只能用于方法和字段的可重复注解类型。重要的是要仔细设计注解类型,以确保使用注解的程序员能认为这些注解尽可能的灵活和强大。

  • 十、类型注释和可插拔类型系统*

在Java SE 8发布之前,注解只能应用于声明。从Java SE 8开始,注解也可以应用于任何类型。这意味着注解可以在任何使用类型的地方使用。比如,类实例创建表达式(new)、转换、实现子句和抛出子句。这种形式的注解称为类型注解。

创建类型注解是为了支持对Java程序进行改进的分析,以确保更强的类型检查。Java SE 8版本不提供类型检查框架,但它允许您编写(或下载)一个类型检查框架。例如,您希望确保程序中的特定变量永远不会被赋值为null;您希望避免触发NullPointerException。您可以编写一个自定义插件来检查这一点。然后,您可以修改代码来注释该特定变量,表明它永远不会被赋值为null。变量声明可能看起来像这样:

@NonNull String str;

当您在命令行编译代码(包括NonNull模块)时,如果编译器检测到潜在问题,则会打印一条警告,允许您修改代码以避免错误。在您更正代码以删除所有警告后,程序运行时将不会出现此特定错误。

您可以使用多个类型检查模块,每个模块检查不同类型的错误。通过这种方式,您可以在Java类型系统的基础上进行构建,在需要时添加特定的检查。

通过明智地使用类型注释和可插拔的类型检查器,您可以编写更强大、更不容易出错的代码。

在许多情况下,您不必编写自己的类型检查模块。有第三方已经完成了这项工作。

十一、课后练习

(一)练习题

1.下面的接口定义有何错误?

public interface House {
    @Deprecated
    void open();
    void openFrontDoor();
    void openBackDoor();
}

2.考虑问题1中所示的House接口的实现为MyHouse。

public class MyHouse implements House {
    public void open() {}
    public void openFrontDoor() {}
    public void openBackDoor() {}
}

如果你编译这个程序,编译器会生成一个警告,因为open被弃用了(在接口中)。你该如何来消除这个警告?

3.以下代码编译时是否会有错误?为什么?

public @interface Meal { ... }

@Meal("breakfast", mainDish="cereal")
@Meal("lunch", mainDish="pizza")
@Meal("dinner", mainDish="salad")
public void evaluateDiet() { ... }

4.为增强请求定义一个注解:包括元素id, synopsis, engineer, 和date。将engineer的默认值指定为unassigned ,将日期的默认值指定为unknown。

(二)答案

1.文档应该反映为什么弃用open方法,以及用什么来代替。例如

public interface House { 
    /**
     * @deprecated use of open 
     * is discouraged, use
     * openFrontDoor or 
     * openBackDoor instead.
     */
    @Deprecated
    public void open(); 
    public void openFrontDoor();
    public void openBackDoor();
}

2.你可以弃用open方法的实现:

public class MyHouse implements House { 
    // The documentation is 
    // inherited from the interface.
    @Deprecated
    public void open() {} 
    public void openFrontDoor() {}
    public void openBackDoor() {}
}

或者,你也可以抑制警告:

public class MyHouse implements House { 
    @SuppressWarnings("deprecation")
    public void open() {} 
    public void openFrontDoor() {}
    public void openBackDoor() {}
}

3.代码无法编译。在 JDK 8 之前,不支持可重复的注释。从 JDK 8开始,由于 Meal 注释类型未被定义为可重复的,因此代码无法编译。可以通过添加 @Repeatable 元注释并定义容器注释类型来解决此问题。

public class AnnotationTest {

    public @interface MealContainer {
        Meal[] value();
    }

    @java.lang.annotation.Repeatable(MealContainer.class)
    public @interface Meal {
        String value();
        String mainDish();
    }

    @Meal(value="breakfast", mainDish="cereal")
    @Meal(value="lunch", mainDish="pizza")
    @Meal(value="dinner", mainDish="salad")
    public void evaluateDiet() { }
}

4.答案如下:

/**
 * 描述了请求增强 (RFE) 注释类型。
 * Describes the Request-for-Enhancement (RFE) annotation type.
 */
public @interface RequestForEnhancement {
    int id();
    String synopsis();
    String engineer() default "[unassigned]";
    String date() default "[unknown]";
}

参考资料:

1.【黑马磊哥】Java反射、注解、反射机制、反射专题、注解专题、挑战100个Java知识点,相信听完这套课,肯定可以解锁Java反射和注解

2.

https://docs.oracle.com/javase/tutorial/java/annotations/index.htmlicon-default.png?t=N7T8https://docs.oracle.com/javase/tutorial/java/annotations/index.html

你可能感兴趣的:(java,开发语言)