Java 注解

文章目录

Java 注解(Annotation)在 Java 5 中引入,用于向程序中添加元数据信息。例如 @Override 就是一个常用的注解,这个注解用于告诉编译器当前方法重写了父类中的方法;lombok.Data 中的 @Data 注解则会自动向类中添加 Getter、Setter 方法和 toStringhashCodeequals 等方法。注解为代码提供额外信息,并且通常不会影响程序执行逻辑。

Java 的内置注解

  1. @Override

@Override 注解用于告诉编译器,当前方法重写了父类中的方法。如果没有重写或者方法签名与父类中不同,编译器则会报错。

  1. @Deprecated

@Deprecated 注解可以用于修饰构造方法、字段、局部变量、方法、包、模块、方法参数、类、接口、注解和枚举类型,该注解用于标记程序元素已经过时。虽然被标记的程序元素仍可使用,但是不推荐继续使。调用被标记为 @Deprecated 的程序元素时,编译器会发出警告,部分 IDE 会使用删除线标记。

  1. @SuppressWarnings

@SuppressWarnings 注解用于抑制编译器生成的特定警告,但不会改变代码行为。该注解可以用于修饰类、接口、注解、枚举、字段、方法、参数、构造方法和局部变量。例如在 Java 反射中使用的 Person 类,由于其中的 addAge 方法未被使用,因此编译器会发出警告 "The method addAge(int) from the type Person is never used locally",如果我在方法前加入 @SuppressWarnings("unused") 注解,编译器则会停止对该方法发出警告。

除了 "unused" 之外,还有其他常见的可抑制警告类型:

警告类型 说明
"unused" 抑制未使用的变量或方法的警告
"unchecked" 抑制未经检查的类型转换警告(泛型相关)
"rawtypes" 抑制使用原始类型(不带泛型)的警告
"deprecation" 抑制使用已弃用方法/类的警告
"serial" 抑制可序列化类缺少 serialVersionUID 的警告
"fallthrough" 抑制 switch 语句中 case 穿透的警告
"finally" 抑制 finally 块无法正常完成的警告
"all" 抑制所有警告(慎用)
  1. @SafeVarargs(JDK 7 引入)

@SafeVarargs 注解用于向解释器声明一个带有泛型可变参数的方法或构造器是安全的,从而抑制相关的警告,相当于对特定的警告使用 @SuppressWarnings("unchecked") 注解。

例如对于下面的方法:

1public static void unsafeOperation(List<String> lists) {
2    Object[] array = lists; // 合法但危险
3    array[0] = Arrays.asList(42); // 堆污染
4    String s = lists[0].get(0); // 运行时 ClassCastExceptio
5    System.out.println(s); // 这行代码永远不会被执行,因为上面的代码会抛出异常
6}

解释器会对这段代码发出关于类型安全的警告,使用 @SafeVarargs 注解可以抑制相应警告。

  1. @FunctionalInterface(JDK 8 引入)

@FunctionalInterface 用于声明一种接口为函数式接口,即接口中有且只有一个抽象方法,但可以有任意多个从 Object 中继承的抽象方法。函数式接口可以被隐式地转换为 Lambda 表达式,如下面的代码:

 1package com.jackgdn;
 2
 3import com.jackgdn.interfaces.Validator;
 4
 5public class Main {
 6    public static void main(String[] args) {
 7        Validator lengthValidator = s -> s != null && !s.trim().isEmpty();
 8        Validator symbolValidator = s -> s.contains("@");
 9        Validator emailValidator = lengthValidator.and(symbolValidator);
10
11        System.out.println(emailValidator.isValid("li_zhong_yao@foxmail.com"));
12        System.out.println(emailValidator.isValid("jackgdn.github.io"));
13    }
14}
15
16/*
17 * output:
18 * true
19 * false
20 */

自定义注解

元注解

元注解是用于修饰注解的注解。Java 中有五种元注解,分别是 @Target@Retention@Documented@InheritedRepeatable

  1. @Target

@Target 注解用于标记一个注解的适用代码元素,其参数为 ElementType 的枚举类型,其中包含如下类型:

类型 说明
ElementType.TYPE 类、接口(包括注解接口)、枚举或记录(record)声明
ElementType.FIELD 字段声明(包括枚举常量)
ElementType.METHOD 方法声明
ElementType.PARAMETER 形式参数声明
ElementType.CONSTRUCTOR 构造函数声明
ElementType.LOCAL_VARIABLE 局部变量声明
ElementType.ANNOTATION_TYPE 注解接口声明(以前称为注解类型)
ElementType.PACKAGE 包声明
ElementType.TYPE_PARAMETER 类型参数声明(如泛型类型的<T>
ElementType.TYPE_USE 任何类型的使用(如强制类型转换、泛型类型参数、异常声明等)
ElementType.MODULE 模块声明(Java 9+ 模块化系统)
ElementType.RECORD_COMPONENT 记录(record)组件(相当于自动生成的字段和访问器)
  1. @Retention

@Retention 用于标记注解的生命周期,其参数是 RetentionPolicy 的枚举类型,其中包含如下类型:

类型 说明
RetentionPolicy.SOURCE 注解仅在源码中保留,编译时丢弃
RetentionPolicy.CLASS 注解会在字节码中保留,运行时丢弃(默认保留策略)
RetentionPolicy.RUNTIME 当程序运行时,JVM 仍会保留注解
  1. @Documented

使用 @Documented 修饰的注解会被包含到生成的 Javadoc 中。

  1. @Inherited

使用 @Inherited 注解标记的注解会继承父类的注解。

  1. @Repeatable

使用 @Repeatable 注解修饰的注解可以在同一代码元素中重复使用。

定义注解

注解可以通过 @interface 关键字定义:

 1package com.jackgdn.annotations;
 2
 3import java.lang.annotation.ElementType;
 4import java.lang.annotation.Retention;
 5import java.lang.annotation.RetentionPolicy;
 6import java.lang.annotation.Target;
 7
 8@Target({ ElementType.FIELD, ElementType.PARAMETER })
 9@Retention(RetentionPolicy.RUNTIME)
10public @interface ValidRange {
11    int min() default 0;
12
13    int max() default 100;
14}

@ValidRange 注解可以用于标记字段和参数,其保留策略为运行时保留策略。注解中定义了两个整形值,其中 min 的默认值为 0,max 的默认值为 100。

注解中可以包含以下类型的元素:

  • 基本类型
  • String
  • Class
  • enum
  • 注解
  • 以上类型的数组

注解处理

注解本身只是标记,因此在定义并使用注解后,我们需要使用注解处理逻辑并且手动调用。一个常用处理注解的方法是利用反射 API。

我们首先修改 Person 类:

 1package com.jackgdn.entities;
 2
 3import lombok.Data;
 4import com.jackgdn.annotations.ValidRange;
 5
 6@Data
 7public class Person {
 8    public String name;
 9
10    @ValidRange
11    private int age;
12    private Gender gender;
13
14    public static enum Gender {
15        MALE, FEMALE, UNKNOWN
16    }
17
18    public Person() {
19        this.name = "Unknown";
20        this.age = 0;
21        this.gender = Gender.UNKNOWN;
22    }
23
24    public Person(String name, int age, Gender gender) {
25        this.name = name;
26        this.age = age;
27        this.gender = gender;
28    }
29
30    @SuppressWarnings("unused")
31    private int addAge(int value) {
32        this.age += value;
33        return this.age;
34    }
35}

修改后的 Person 类的 age 字段使用 ValidRange 注解修饰,这要求 age 的范围只能在 $[0, 100]$ 之间。

随后定义注解处理逻辑:

 1package com.jackgdn.utils;
 2
 3import java.lang.reflect.Field;
 4
 5public class ValidRangeProcessor {
 6    public static boolean validateRange(Object obj) {
 7        Class<?> clazz = obj.getClass();
 8        // 便利所有字段
 9        for (Field field : clazz.getDeclaredFields()) {
10            // 判断是否有 ValidRange 注解
11            if (field.isAnnotationPresent(com.jackgdn.annotations.ValidRange.class)) {
12                com.jackgdn.annotations.ValidRange range = field
13                        .getAnnotation(com.jackgdn.annotations.ValidRange.class);
14                field.setAccessible(true); // 允许访问私有字段
15                try {
16                    int value = (int) field.get(obj);
17                    // 校验范围
18                    if (value < range.min() || value > range.max()) {
19                        return false;
20                    }
21                } catch (IllegalAccessException e) {
22                    e.printStackTrace();
23                    return false;
24                }
25            }
26        }
27        return true;
28    }
29}

编写主类:

 1package com.jackgdn;
 2
 3import com.jackgdn.entities.Person;
 4import com.jackgdn.utils.ValidRangeProcessor;
 5
 6public class Main {
 7    public static void main(String[] args) {
 8        Person jack = new Person("Jack", 120, Person.Gender.MALE);
 9        if (!ValidRangeProcessor.validateRange(jack)) {
10            jack.setAge(0);
11        }
12        System.out.println(jack.getAge());
13
14        Person jane = new Person("Jane", 20, Person.Gender.FEMALE);
15        if (!ValidRangeProcessor.validateRange(jane)) {
16            jane.setAge(0);
17        }
18        System.out.println(jane.getAge());
19    }
20}
21
22/*
23 * output:
24 * 0
25 * 20
26 */