Java 注解学习笔记

49 阅读11分钟

Java注解(Annotation),也被称为元数据,是JDK1.5及以后版本引入的一个特性。它是一种代码级别的说明,用于为Java代码提供额外的信息或元数据,这些信息或元数据不与代码的执行直接相关,但可以被编译器或运行时环境用来执行额外的处理或检查。注解可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明或注释。

一、注解的作用

  1. 编写文档:通过代码里标识的元数据生成文档(如doc文档)。这有助于开发人员更好地理解和维护代码。
  2. 编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查。例如,@Override注解用于标记方法是否成功地覆盖了父类中的方法,如果没有实际覆盖,则编译器会发出错误警告。
  3. 代码生成:注解可以通过编译时的处理器(Annotation Processor)生成额外的代码文件,从而实现框架的自动化配置、代码的自动生成等功能。
  4. 运行时处理:注解可以在运行时通过反射机制读取和处理,用来实现一些动态的行为,如依赖注入、AOP(面向切面编程)等。

二、注解分类

  • 元注解: java内置的注解,标明该注解的使用范围、生命周期等
  • 标准注解: Java提供的基础注解,标明过期的元素/标明是复写父类方法的方法/标明抑制警告。
  • 自定义注解-第三方定义的注解,含义和功能由第三方来定义和实现

三、元注解

元注解包括@Retention@Target@Inherited@Documented@Repeatable等。这些注解用于指定其他注解的行为和特性。

1 @Retention

指定注解的保留策略,即注解在何时有效。可选的保留策略包括:

  • SOURCE:仅保留在源代码中
  • CLASS:保留在class文件中,但JVM加载时不保留
  • RUNTIME:保留在class文件中,可通过反射机制读取
// 使用如:
@Retention(RetentionPolicy.RUNTIME)

2 @Target

描述了注解的使用范围,允许自定义注解标注在哪些Java元素上(类、方法、属性、局部属性、参数....)
@Target注解的value属性是一个ElementType枚举数组,以下是ElementType枚举的一些常见值:

说明
TYPE允许被修饰的注解作用在类、接口(包括注解类型)或枚举上
FIELD允许作用在属性字段上,包括枚举的常量
METHOD允许作用在方法上
PARAMETER允许作用在方法参数上
CONSTRUCTOR允许作用在构造器(构造方法)上
LOCAL_VARIABLE允许作用在本地局部变量上,例如方法内部的变量
ANNOTATION_TYPE允许作用在另一个注解上,即元注解
PACKAGE允许作用在包上,但注意,在Java中直接应用于包的注解不常见,因为包声明不包含修改器(如public、protected等),并且包注解通常通过特定的工具或库来处理
TYPE_PARAMETER(Java 8 引入)表示该注解能写在类型变量的声明语句中
TYPE_USE(Java 8 引入)表示该注解能写在使用类型的任何语句中

3. @Inherited

表示注解类型将自动被继承到子类中,如果子类中没有指定同类型的注解。注意: 只有当子类继承父类的时候,注解才会被继承,而类实现接口,或者接口继承接口,都是无法获得注解上的注解声明的。
使用示例:

import java.lang.annotation.Inherited;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
import java.lang.annotation.ElementType;  
  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
@Inherited  
public @interface MyAnnotation {  
    // 注解定义  
}  
  
@MyAnnotation  
public class Parent {  
    // ...  
}  
  
public class Child extends Parent {  
    // Child类会自动继承Parent类上的@MyAnnotation注解  
}

在上述示例中,@MyAnnotation注解被@Inherited修饰,因此当它被应用于Parent类时,Child类作为Parent的子类,也会自动继承@MyAnnotation注解。

注意事项:

  • 反射查询:在使用反射查询类、方法或字段的注解时,如果某个类或其父类(或超类)上被应用了使用了@Inherited的注解,那么通过反射查询该类的注解时,也会返回这个继承自父类的注解。
  • 接口实现:如上所述,@Inherited注解不适用于接口及其实现类之间的关系。如果一个注解被应用于接口,那么该接口的实现类不会自动继承该注解。

4. @Documented

它告诉javadoc和类似的文档生成工具,被其标记的注解类型应当被视为公共API的一部分,并且在生成的文档中应当包含这些注解类型的描述和使用方法。

5. @Repeatable

用于标注某个注解可以在同一个声明(如类、方法、字段等)上多次使用。
在Java 8之前,一个注解在同一个地方只能被声明一次,这限制了注解的灵活性和表达能力。@Repeatable注解的引入解决了这个问题,使得开发者可以更加方便地表达需要多次使用某个注解的场景。

// 使用@Repeatable标注Role注解  
@Repeatable(Roles.class)  
public @interface Role {  
    String value();  
}  
  
// 容器注解(保持不变)  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.TYPE)  
public @interface Roles {  
    Role[] value();  
}  
  
// 使用方式(无需显式使用@Roles)  
@Role("ADMIN")  
@Role("USER")  

你可以直接在@Role注解上使用@Repeatable注解,并指定容器注解(如@Roles),然后在需要的地方多次使用@Role注解,而无需显式使用容器注解。

四、标准注解

1.@Override

1.1 主要用途

  • 编译时检查@Override 注解允许编译器检查你是否真的重写了父类或接口中的方法。如果方法签名不匹配,或者父类中根本不存在该方法,编译器会报错。这有助于在编译阶段就发现潜在的错误。
  • 代码清晰度:通过在方法上使用 @Override 注解,其他开发者(包括未来的你)可以更容易地理解该方法的作用,即它是为了重写父类或接口中的某个方法。
  • 避免错误:有时候,我们可能会因为方法名拼写错误或参数列表的微小差异而意外地创建了一个新方法,而不是重写父类中的方法。使用 @Override 注解可以避免这种情况,因为编译器会检查方法签名的正确性。

1.2 使用场景

  • 当你从父类继承并重写了一个方法时。
  • 当你实现了接口中的方法时(尽管对于接口中的方法,使用 @Override 不是必需的,但它仍然是一个好习惯,可以提高代码的可读性)。

1.3 注意事项

  • @Override 注解只能用于方法上,不能用于其他类型的成员(如字段、构造器、初始化块等)
  • 继承自 object 类的方法(如 toString()equals(Object obj)hashCode() 等。也可以被重写,并且这些方法上也可以用@Override 注解。
  • 如果一个方法被标记为 final,则他不能被重写,因此也不能再该方法上使用 @Override 注解
  • 构造方法、静态方法和私有方法都不能被重写,因此这些方法上也不能使用@Override 注解

1.4 使用示例

class Animal {  
    public void eat() {  
        System.out.println("This animal eats food.");  
    }  
}  
  
class Dog extends Animal {  
    @Override  
    public void eat() {  
        System.out.println("Dog eats meat.");  
    }  
}  
  
interface Speaker {  
    void speak();  
}  
  
class GreetingSpeaker implements Speaker {  
    @Override  
    public void speak() {  
        System.out.println("Hello!");  
    }  
}

2.@Deprecated

用于表示某个程序元素(类、方法、字段等)已经过时,不推荐使用,将来可能会被移除。使用这个注解的目的是为了向开发者标明,虽然当前的元素仍然可以使用,但应该寻找并使用更好的替代方案。

2.1 使用场景

  • 当一个类、方法或字段的设计存在缺陷,或者有更优的实现方式时,开发者可以使用 @Deprecated 注解来标记它,并通常会在文档中提供替代方案的说明。
  • 在 API 的演进过程中,某些旧的功能或方法可能会因为技术更新或安全原因而被弃用。使用 @Deprecated 注解可以通知用户这些元素即将被移除,并鼓励他们迁移到新的 API 上。

2.2 注意事项

  • 使用 @Deprecated 注解时,应该同时提供替代方案的说明,以帮助用户进行迁移。
  • 标记为 @Deprecated 的元素并不会立即从 Java 平台中移除,它们可能会在未来的版本中继续存在一段时间,以便给开发者足够的时间来更新他们的代码。
  • 编译器在编译时会对使用了 @Deprecated 注解的元素发出警告,但这不是错误,代码仍然可以正常运行。然而,为了保持代码的健売性和可维护性,建议尽快更新到推荐的替代方案。

2.3 使用示例

// 假设这是一个旧的、不推荐使用的类  
@Deprecated  
public class OldClass {  
    // 旧的、不推荐使用的方法  
    @Deprecated  
    public void oldMethod() {  
        System.out.println("This method is deprecated. Use newMethod() instead.");  
    }  
  
    // 新的、推荐使用的方法  
    public void newMethod() {  
        System.out.println("This is the new method.");  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        OldClass obj = new OldClass();  
        // 这里会收到编译器的警告,因为 oldMethod() 被标记为 @Deprecated  
        obj.oldMethod();  
  
        // 使用新的方法,不会收到警告  
        obj.newMethod();  
    }  
}

3. @SuppressWarnings

用于告诉编译器忽略指定的警告信息。这个注解常用于减少编译器生成的无关紧要的警告,使得开发者可以更加专注于代码的逻辑和重要的错误。

3.1 使用方式

@SuppressWarnings 可以放在类、接口、字段、方法或参数声明的上方。它接受一个字符串数组作为参数,每个字符串指定了一种或多种要忽略的警告类型。

3.2 语法

`` @SuppressWarnings("warningType")
public void someMethod() {
// 编译器会忽略这里可能产生的指定类型的警告
}

@SuppressWarnings({"warningType1", "warningType2"})
public class SomeClass {
// 编译器会忽略这个类中可能产生的指定类型的警告
} ``

3.3 常见警告类型

类型说明
unchecked用于泛型操作中,当使用了未经检查的转换或原始类型时
deprecation用于使用了已过时的方法、类、字段或接口时
fallthrough在 switch 语句中,当 case 分支没有通过 break 或其他跳转语句终止时(注意:从 Java 7 开始,这个警告更多地是通过 @SwitchLabelFallThrough 注解来处理)
unused用于未使用的变量、方法或导入的类
serial当序列化类缺少 serialVersionUID 字段时
rawtypes用于泛型代码中使用了原始类型(raw types)
nullable(非标准,取决于IDE或静态代码分析工具)用于标记可能为 null 的变量或返回值。

3.4 使用示例

`` @SuppressWarnings("unchecked")
public void useRawList() {
List list = new ArrayList(); // 使用原始类型 ArrayList,会产生 unchecked 警告
list.add("Hello");
String item = (String) list.get(0); // 强制类型转换,也会产生 unchecked 警告
}

@Deprecated
public void deprecatedMethod() {
// 这个方法已经过时
}

@SuppressWarnings("deprecation")
public void useDeprecatedMethod() {
deprecatedMethod(); // 使用过时的方法,但由于 @SuppressWarnings 注解,不会产生警告
} ``

五、自定义注解

在Java中,自定义注解是一种强大的功能,允许你为代码(类、方法、变量等)添加元数据。这些元数据可以被编译器或运行时环境读取,以执行各种任务,如代码检查、框架配置等。

1. 自定义注解步骤

  • 定义注解:使用@interface关键字来定义一个注解。注解内部可以定义元素(类似于接口中的方法),但注解的元素默认存在默认值,且必须指定default值(对于基本类型、StringClass类型等),或者元素本身可设置为@java.lang.annotation.Optional(Java 8及以后版本)。
  • 使用注解:将注解应用于类、方法、参数、变量等上。
  • 处理注解:通过反射或其他机制读取注解信息,并根据这些信息执行相应的操作

2. 示例

2.1 定义注解

import java.lang.annotation.ElementType;  
import java.lang.annotation.Retention;  
import java.lang.annotation.RetentionPolicy;  
import java.lang.annotation.Target;  
  
// 指定注解可以应用的Java元素类型  
@Target({ElementType.METHOD, ElementType.TYPE})  
// 指定注解的保留策略,运行时保留意味着可以通过反射读取  
@Retention(RetentionPolicy.RUNTIME)  
public @interface MyAnnotation {  
    // 定义注解元素,没有指定默认值,使用时必须提供  
    String description() default "No description";  
  
    // 另一个注解元素,提供了默认值  
    int value() default 0;  
}

2.2 使用注解

@MyAnnotation(description = "This is a test class", value = 10)  
public class TestClass {  
  
    @MyAnnotation(description = "This method does something")  
    public void testMethod() {  
        // 方法实现  
    }  
}

2.3 处理注解

import java.lang.reflect.Method;  
  
public class AnnotationProcessor {  
  
    public static void processClass(Class<?> clazz) {  
        // 检查类上是否有MyAnnotation注解  
        if (clazz.isAnnotationPresent(MyAnnotation.class)) {  
            MyAnnotation myAnnotation = clazz.getAnnotation(MyAnnotation.class);  
            System.out.println("Class Description: " + myAnnotation.description());  
            System.out.println("Class Value: " + myAnnotation.value());  
        }  
  
        // 遍历类的方法,检查方法上是否有MyAnnotation注解  
        Method[] methods = clazz.getDeclaredMethods();  
        for (Method method : methods) {  
            if (method.isAnnotationPresent(MyAnnotation.class)) {  
                MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);  
                System.out.println("Method Description: " + myAnnotation.description());  
                System.out.println("Method Value: " + myAnnotation.value());  
            }  
        }  
    }  
  
    public static void main(String[] args) {  
        processClass(TestClass.class);  
    }  
}