Java 中的注解

注解概述

注解(Annotation),也叫元数据。一种代码级别的说明。它是 JDK1.5 及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

简单来说,就是说明程序的。给计算机看的。这里要说一下注释:用文字描述程序的。给程序员看的。

作用分类

  1. 编写文档:通过代码里标识的注解生成文档【生成文档 doc 文档】
  2. 代码分析:通过代码里标识的注解对代码进行分析【使用反射】
  3. 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】

JDK 中预定义的一些注解

@Override

检测被该注解标注的方法是否是继承自父类 (接口) 的

img

上例中,该类继承了父类 Object 类,重写了 toString()方法,用 @Override 注解进行标示。而 toString1() 方法不是重写其继承的父类、超类或接口中的方法,用 @Override 注解进行标示,会出现编译错误(Method does not override method from its superclass)。

@Deprecated

该注解标注的内容,表示已过时

img

创建了 method1 方法,不过后来发现有更好的方式,可以实现 method1 方法的功能,便创建了 method2 方法。可是为了 method1 方法还能使用,便用 @Deprecated 注解进行标示,标示该方法已经过时了,如果我们继续调用 method1 方法,如上图所示,方法中出现一条横线。

@SuppressWarnings

压制警告

img

表示在注释元素(以及注释元素中包含的所有程序元素)中应该抑制命名的编译器警告。 请注意,给定元素中抑制的一组警告是所有包含元素中抑制的警告的超集。 例如,如果您注释一个类来抑制一个警告并注释方法来抑制另一个警告,则两个警告将在该方法中被抑制。一般传递参数 all —— @SuppressWarnings("all")

自定义注解:格式和本质

格式

复制
元注解
public @interface 注解名称 {
属性列表;
}

本质

定义一个注解:

public @interface MyAnnotation {
/*
属性:接口中的抽象方法
*/
public abstract String method();
}

注解本质上就是一个接口,MyAnnotation 接口默认继承 Annotation 接口。接口中可以定义的成员方法是接口的属性。

复制
public interface MyAnnotation extends java.lang.annotation.Annotation {}

public interface Annotation 接口,是所有注释类型扩展的公共接口。

自定义注解:属性定义

属性

接口中的抽象方法。

要求

  1. 属性的返回值类型有下列取值

    • 基本数据类型

    • String

    • 枚举

      复制
      public enum Person {
      P1, P2
      }
    • 注解

      复制
      public @interface MyAnnotation2 {
      }
    • 以上数据类型的数组
      img
      使用的时候:
      img

  2. 定义了属性,在使用时需要给属性赋值
    如:定义一个注解
    img
    在使用的时候:
    img

    • 如果定义属性时,使用 default 关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
      如:定义一个注解
      img
      在使用的时候:
      1、使用默认值:"LeeHua"
      img
      2、不使用默认值:
      img

    • 如果只有一个属性需要赋值,并且属性的名称是 value,则 value 可以省略,直接定义值即可。
      如:定义一个注解
      img
      使用的时候:可以省略 value,也可以不省略 value,如下
      img

      img

    • 数组赋值时,值使用 {} 包裹。如果数组中只有一个值,则 {} 可以省略
      如:定义一个注解
      img
      使用的时候,{} 可以省略,也可以不省略:
      img

      img

元注解

简单来说,元注解就是描述注解的注解。如 @Override

img

这里的 @Target 和 @Retention 就是元注解。

四个元注解

@Target
// 描述注解能够作用的位置
@Retention
// 描述注解被保留的阶段
@Documented
// 描述注解是否被抽取到 api 文档中
@Inherited
// 描述注解是否被子类继承

@Target

复制
public @interface Target {
/**
* 返回注释类型可应用于的元素种类的数组。
*/
ElementType[] value();
}

ElementType[] 是一个枚举数组:

/
* 这个枚举类型的常量提供了在 Java 程序中可能出现注释的句法位置的简单分类。
* 在 java.lang.annotation.Target 元注释中使用这些常量来指定写入给定类型的
* 注释的合法位置。
/
public enum ElementType {
/* 类,接口(包括注释类型)或枚举声明 /
TYPE,
/* 字段声明(包括枚举常数) /
FIELD,
/* 方法声明 /
METHOD,
/* 形式参数声明 /
PARAMETER,
/* 构造函数声明 /
CONSTRUCTOR,
/* 局部变量声明 /
LOCAL_VARIABLE,
/* 注释类型声明 /
ANNOTATION_TYPE,
/* 包声明 */
PACKAGE,
/
* 类型参数声明
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 使用类型
*
* @since 1.8
*/
TYPE_USE
}

举例

自定义一个注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) // 表示 MyAnnotation7 注解只能作用于类上
public @interface MyAnnotation7 {
}

该注解的使用:

img

在这里,MyAnnotation7 注解作用在类上是可以的。不过,作用在方法上,或成员变量上、方法上、... ... ,是不可以的。

假如想要作用于成员变量上、方法上、... ... ,可以根据 public enum ElementType {... ...} 在调用元注解的注解上进行添参,从而实现更多的作用域,如:

import java.lang.annotation.ElementType;
@Target(ElementType.TYPE, , ElementType.METHOD, ElementType.FIELD)
// 表示 MyAnnotation7 注解能作用于类、方法、成员变量上
public @interface MyAnnotation7 {
}

@Retention

复制
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* @return 保留策略。
*/
RetentionPolicy value();
}

RetentionPolicy[] 是一个枚举数组:

/
* 注释保留策略。 此枚举类型的常量描述了用于保留注释的各种策略。 它们
* 与@Retention元注释类型一起使用,以指定将保留注释多长时间。
/
public enum RetentionPolicy {
/* 注释将被编译器丢弃。 /
SOURCE,
/* 注释将由编译器记录在类文件中,但 JVM 不需要在运行时保留。 */
CLASS,
/
* 注释将由编译器记录在类文件中,并由 JVM 在运行时保留,因此可以反射读取。
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}

举例

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
// 当前被描述的注解(MyAnnotation8),会保留到 class 字节码文件中,并被 JVM 读取到
public @interface MyAnnotation8 {
}

@Documented

复制
/**
* 表示具有类型的注释默认情况下由 javadoc 和类似工具记录。
* 即:描述注解是否被抽取到 api 文档中
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

举例

自定义一个注解:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Documented;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
// 表示 MyAnnotation7 注解能作用于类、方法、成员变量上
@Retention(RetentionPolicy.RUNTIME)
// 当前被描述的注解(MyAnnotation8),会保留到 class 字节码文件中,并被 JVM 读取到
@Documented
// 描述注解会被抽取到 api 文档中
public @interface MyAnnotation9 {
}

使用该注解:

@MyAnnotation9
public class Demo09Annotation {
private String name = "LeeHua";
@MyAnnotation9
public void method() {
}
}

在终端,进行编译:

复制
cd /Users/liyihua/IdeaProjects/Study/src/view/study/demo47/
javadoc Demo09Annotation.java

javadoc 编译完成后,生成一大堆文件:
img

img

img

img

Google 浏览器打开 index-all.html 文件,可以查看被抽取到 API 文档中的描述:

img

@Inherited

复制
/** 描述注解是否被子类继承 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

举例

自定义一个注解:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
// 表示 MyAnnotation7 注解能作用于类、方法、成员变量上
@Retention(RetentionPolicy.RUNTIME)
// 当前被描述的注解(MyAnnotation8),会保留到 class 字节码文件中,并被 JVM 读取到
@Documented
// 描述注解会被抽取到 api 文档中
@Inherited
// 描述注解是否被子类继承
public @interface MyAnnotation10 {
}

父类使用这个注解:

@MyAnnotation10
public class Demo09Annotation {
}

子类继承父类:

public class Demo10Annotation extends Demo09Annotation {
}

在 @Inherited 注解描述的情况下,父类使用了 @MyAnnotation10 注解,子类继承父类,也会使用 @MyAnnotation10 注解。

案例一

需求

写一个 "框架",不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法

实现之前,定义 Person.java、Student.java:

package view.study.demo48;
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
public void personMethod() {
System.out.println("我是 Person 中的方法!!!");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package view.study.demo48;
public class Student {
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
public void studentMethod() {
System.out.println("我是 Student 中的方法!!!");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

实现

定义一个注解:

package view.study.demo48;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) // 注解能作用于类上
@Retention(RetentionPolicy.RUNTIME) // 当前被描述的注解,会保留到 class 字节码文件中,并被 JVM 读取到
public @interface pro {
public abstract String className();
public abstract String methodName();
}

定义实现类:

package view.study.demo48;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 写一个 "框架",不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法
* 利用:反射、注解实现
*/
@pro(className = "view.study.demo48.Person", methodName = "personMethod")
public class Demo01Reflection {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException,
InstantiationException, NoSuchMethodException, InvocationTargetException {
// 1. 解析注解
// 1.1 获取 Demo01Reflection 类的字节码文件对象
Class<Demo01Reflection> drClass = Demo01Reflection.class;
// 2. 获取上边的注解对象:@pro(className = "view.study.demo48.Person", methodName = "personMethod")
pro annotation = drClass.getAnnotation(pro.class);
// 3. 调用注解中定义的抽象方法,获取返回值
// 3.1 获取 className
String className = annotation.className();
// 3.2 获取 methodName
String methodName = annotation.methodName();
// 4. 加载该类(获取到的类)进内存
Class<?> aClass = Class.forName(className);
// 5. 创建对象
Object object = aClass.newInstance();
// 6. 获取方法对象
Method method = aClass.getMethod(methodName);
// 7. 执行方法
method.invoke(object);
}
}

第二步:

// 第二步其实就是在内存中生成了一个该注解接口的子类实现对象
public class proImpl implements pro {
// 实现 pro 接口中的 className()方法和 methodName() 方法
public String className() {
return className;
}
public String methodName() {
return methodName;
}
}

运行程序,控制台输出:

复制
我是 Person 中的方法!!!

修改注解中的 className、methodName:

复制
@pro(className = "view.study.demo48.Student", methodName = "studentMethod")

再次运行程序,控制台输出:

复制
我是 Student 中的方法!!!

案例二

需求

利用反射、注解,写一个简单的测试框架,当主方法执行后,会自动加载被检测的所有方法,判断是否有异常,并记录到文件中。注解名称是 @Check

被测试的方法如下:

package view.study.demo48;
public class Calculator {
/** 加法 /
@Check
public int add(int a, int b) {
return a + b;
}
/* 减法 /
@Check
public int sub(int a, int b) {
return a - b;
}
/* 乘法 /
@Check
public int mul(int a, int b) {
return a * b;
}
/* 除法 */
@Check
public int div(int a, int b) {
return a / b;
}
public void method() {
System.out.println("永无 bug!!!");
}
}

实现

自定义一个注解:

package view.study.demo48;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME) // 当前被描述的注解,会保留到 class 字节码文件中,并被 JVM 读取到
@Target(ElementType.METHOD) // 注解能作用于方法
public @interface Check {
}

主方法实现类:

package view.study.demo48;
import java.io.IOException;
import java.io.FileWriter;
import java.io.BufferedWriter;
import java.lang.reflect.Method;
public class Demo01Calculator {
public static void main(String[] args) throws IOException {
// 1. 创建计算器对象
Calculator calculator = new Calculator();
// 2. 通过计算器对象,获取字节码文件
Class<? extends Calculator> aClass = calculator.getClass();
// 3. 通过字节码文件,获取 calculator 中所有的方法
Method[] methods = aClass.getDeclaredMethods();
// 4. 定义异常出现的次数、创建字符缓冲输入流
int count = 0;
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("bug.txt", true));
// 5. 判断方法上是否有 "@Check" 注解
for (Method method : methods) {
if (method.isAnnotationPresent(Check.class)) {
try {
// 6. 如果方法上有 "@Check" 注解,那么执行该方法
method.invoke(calculator, 1, 0);
} catch (Exception e) {
// 7. 捕获异常,记录到文件中
count ++;
bufferedWriter.newLine();
bufferedWriter.write("被测试的方法:" + method.getName() + ",该方法出现异常。");
bufferedWriter.newLine();
bufferedWriter.write("异常的名称:" + e.getCause().getClass().getSimpleName());
bufferedWriter.newLine();
bufferedWriter.write("异常的原因:" + e.getCause().getMessage());
bufferedWriter.newLine();
}
}
}
bufferedWriter.write("本次测试一共出现了" + count + "次异常。");
bufferedWriter.newLine();
bufferedWriter.write("====================================");
// 8. 释放 BufferedWriter 资源
bufferedWriter.flush();
bufferedWriter.close();
}
}

运行主方法,bug.txt 文件中会写入一定的内容,内容如下:

复制
被测试的方法:div,该方法出现异常。
异常的名称:ArithmeticException
异常的原因:/ by zero
本次测试一共出现了 1 次异常。
====================================

分析:

要被检测的方法有四个,分别是:加法、减法、乘法、除法。

主方法中传入的参数 a、b 是:1 和 0

很明显,1 除 0,是无意义的,所以 div 方法出现异常,异常名称是:ArithmeticException

其他方法没有出现异常。所以出现的异常次数是:1