# Java教程 - 3 注解

Java中的注解(Annotation)是从 JDK 5.0开始引入的一种元编程的语法工具,它用于为代码提供 元数据。注解可以添加加到包、类、方法、字段、参数和局部变量上。

不举例子,说了和没说一样。

# 3.1 JDK自带注解举例

先举一些 JDK 自带的注解的例子。

# 1 @Override

@Override 注解用于标记一个方法,表示该方法在重写父类中的方法。如果父类中没有该方法,或者方法的签名和父类中的不匹配,编译器会报错。

举个栗子:

class Parent {  
    void show() {  
        System.out.println("Parent's show()");  
    }  
}  
  
class Child extends Parent {  
    @Override  
    void show() {  
        System.out.println("Child's show()");  
    }  
}
1
2
3
4
5
6
7
8
9
10
11
12

在这个例子中,Child 类中的 show 方法使用 @Override 注解来表明它正在重写 Parent 类中的 show 方法。

如果给 Child 类中的 show 方法添加参数,那么和父类中的方法签名不一样,就无法实现重写父类的方法了,编译就会报错。

我们在重写父类或接口中的方法的时候,建议添加 @Override 注解,这样可以很清晰的知道这个方法是重写的方法。

# 2 @Deprecated

@Deprecated 用于标记一个元素(类、方法、字段等)已过时,不建议使用,将来的版本中可能会被移除。当其使用这些已过时的元素时,编译器会发出警告,也是可以用的。

举个栗子:

我自己定义一个 OldClass 类,添加 @Deprecated 注解,然后使用这个类。

@Deprecated  
class OldClass {  

}  
  
public class AnnotationTest {  
    public static void main(String[] args) {  
        OldClass obj = new OldClass(); // 这里会收到编译器的警告,因为 OldClass 已被标记为 @Deprecated  
    }  
}
1
2
3
4
5
6
7
8
9
10

上面代码看不出区别,在 IDEA 中就会有提示,直接有横杠,提示过期,不建议使用:

# 3.2 自定义注解

上面说的是 JDK 自带的注解,在实际的开发中,自定义注解才能发挥注解的强大作用。在 Java Web开发中,现在用的 SpringMVC、SpringBoot、Mybatis等框架,都是使用注解进行各种配置,没有注解是无法简单的实现这些优秀的框架的,可能需要 xml 配置,非常不方便,可以说 框架 = 注解+反射+设计模式,反射后面再讲,没有反射,注解也发挥不出作用。

下面讲一下如何自定义注解。

# 1 定义注解

举个栗子:

定义一个名称为 MyAnnotation 的注解:

public @interface MyAnnotation {
  
}
1
2
3

语法和定义接口十分相似,在 interface 前面加 @ ,但是注解和接口没有什么关系,只是自定义注解自动继承了**java.lang.annotation.Annotation** 接口。

# 2 添加属性

注解中也是可以添加属性的,不过和类中的属性有些区别,使用无参数方法的形式来定义。

举个栗子:

public @interface MyAnnotation {
    String value();
    int index();
}
1
2
3
4

上面的注解中,定义了两个属性,valueindex。如果只有一个参数成员,推荐属性名使用 value。属性的类型支持8种基本数据类型(int, boolean, char, long, double, float, byte, short 以及它们的包装类),StringClass,枚举类型,注解类型,以及数组上述类型的数组。


也可以为属性设置默认值:

public @interface MyAnnotation {
    String value() default "www.doubibiji.com";
    int index();
}
1
2
3
4

上面为 value 设置了默认值为 www.doubibiji.com

# 3.3 使用注解

上面定义了注解,现在可以使用注解了。

定义一个 Student 类,在类上使用 MyAnnotation 注解。

@MyAnnotation(value="doubi", index=0)
class Student{
    
}
1
2
3
4

先说明一下使用:

  • 如果注解中有属性,那么在后面的括号中就需要传值,格式是 参数名=参数值,如果注解中的某个属性有默认值,那么该属性可以不传值;
  • 如果注解中只有一个属性,并且名称为 value,可以省略 value= ,直接 @MyAnnotation("doubi")

上面为 Student 类添加了自定义的 MyAnnotation 注解,有什么作用呢?

卵用没有!

因为没有指定该注解什么时候生效(其实默认是编译的时候生效),是编译的时候生效还是运行的时候生效。

这里就需要使用元注解来进行标识了。

# 3.4 元注解

什么是元注解?

元注解就是为注解提供注解的注解,就是使用在注解上的注解。元注解的主要作用在于控制注解的行为,包括注解的保留策略、作用目标(可以使用在类上啊,还是方法上啊)等。

元注解有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种,下面来介绍一下它们的作用。

# 1 @Retention

@Retention 元注解用于指定注解的保留策略。

举个栗子:

下面自定义了 MyAnnotation 注解,在注解上使用了 @Retention 元注解,并设置了值为 RetentionPolicy.RUNTIME

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

@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
1
2
3
4
5
6
7

什么意思呢,当我们使用 @MyAnnotation 注解的时候,指定了 @MyAnnotation 注解的保存策略。

@MyAnnotation("doubi")
class Student{
}
1
2
3

RetentionPolicy.RUNTIME 表示在运行 Java 程序的时候,注解依然在,在程序运行的时候,可以通过反射来获取 Student 类上的 @MyAnnotation 注解,如果想通过反射来获取注解(后面再讲),就需要设置为这种策略。

@Retention 还有两个取值:

  • RetentionPolicy.SOURCE :在源文件中有效,但是在编译的时候,不会编译到 class 文件中,编译器直接丢弃这种策略的注解。

    查看 Override 注解,你会发现就是这种策略,只是在编译期有效,提示你程序有没有问题,但是不会编译到 class文件中。

  • RetentionPolicy.CLASS:会保留在 class 文件中,当运行 Java 程序时,JVM 不会保留注解,也就是说不会被加载入内存中,这是默认策略。

# 2 @Target

@Target 元注解的作用就是用来标识注解能修饰哪些元素,能修饰类呀,还是方法呀,还是其他元素。

举个栗子:

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)
public @interface MyAnnotation {
    String value();
}
1
2
3
4
5
6
7
8
9
10

在上面使用 @Target 元注解,标识 @MyAnnotation 注解能修饰的元素类型是 ElementType.TYPE,表示只能用来修饰类。


当然可以指定多个可以修饰的元素:

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
1
2
3
4
5

上面指定了 MyAnnotation 注解可以修饰的元素有 ElementType.TYPE(类),ElementType.METHOD(方法), ElementType.FIELD(属性)。

ElementType 的常用的枚举值有:

枚举值 解释 枚举值 解释
ElementType.TYPE 用于类、接口(包括注解类型)或枚举声明 ElementType.METHOD 用于修饰方法
ElementType.FIELD 用于修饰属性 ElementType.PARAMETER 用于修饰参数
ElementType.CONSTRUCTOR 用于修饰构造方法 ElementType.LOCAL_VARIABLE 用于修饰局部变量
ElementType.PACKAGE 用于修饰包
ElementType.TYPE_PARAMETER 可以用来修饰泛型类型,JDK8新增 ElementType.TYPE_USE 修饰类型,JDK8新增

ElementType.TYPE_PARAMETERElementType.TYPE_USE 再举个栗子:

如果 @MyAnnotation 注解的 target 添加 ElementType.TYPE_PARAMETER,那么可以修饰泛型:

class Student<@MyAnnotation("doubi") T>{
}
1
2

如果 @MyAnnotation 注解的 target 添加 ElementType.TYPE_USE,那么可以修饰类型:

// 修饰异常类型一
public static void main(String[] args) throws @MyAnnotation("doubi") RuntimeException {

    // 修饰类型二
    List<@MyAnnotation("hello") String> list = new ArrayList<>();

    // 修饰类型三
    @MyAnnotation("int") int num = 10;
}
1
2
3
4
5
6
7
8
9

如果不指定修饰的元素的类型,那么可以用在所有元素上。

一般我们定义注解,主要就是使用 @Retention@Target 元注解。

下面三个我们自己用的不多。

# 3 @Documented

我们在写代码的时候,会写注释,在注释中会用到一些注解。

举个栗子:

/**
 * 测试注解的类
 * @author 逗比笔记
 * @version 1.0.0
 */
public class AnnotationTest {
    /**
     * main 方法
     * @param args
     * @return void
     */
    public static void main(String[] args) {
        
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上面注释中的这些注解是 JDK 自带的文档注解,在生成 javadoc 文档的时候,会被提取出来。

如果我们自定义注解,想在生成 javadoc文档的时候也被提取出来,那就需要使用 @Documented 元注解,@Documented 元注解的作用就是用于指定被修饰的注解是否会被 javadoc 工具提取成文档。默认情况下,javadoc 是不包括注解的。

// 使用@Documented注解
@Documented
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
1
2
3
4
5
6
7

# 4 @Inherited

@Inherited 元注解用于指定被修饰的注解是否被子类继承。如果一个类使用了被 @Inherited 修饰的注解,那么它的子类也会自动拥有这个注解。

举个栗子:

首先我们先定义一个注解,使用 @Inherited 修饰:

import java.lang.annotation.*;

@Inherited
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
1
2
3
4
5
6
7
8

然后使用 MyAnnotation 注解修饰 Person 类:

@MyAnnotation("doubi")
class Person {

}

class Student extends Person{

}
1
2
3
4
5
6
7
8

因为 Student 类继承了 Person 类,Person 类使用了 @MyAnnotation 注解,所以 Student 类也拥有了 @MyAnnotation 注解。

# 5 @Repeatable

@Repeatable 是 Java 8 中引入的一个元注解,它允许在同一个元素(如类、方法或字段)上重复使用相同的注解。在 @Repeatable 出现之前,Java 的注解是不支持在同一个元素上重复使用的,这限制了注解的灵活性和表达力。

要使用 @Repeatable 注解,你需要定义两个注解:

  1. 目标注解:这是你想要重复使用的注解。
  2. 容器注解:这是一个特殊的注解,用于存储目标注解的多个实例。这个容器注解需要使用 @Repeatable 来修饰,并指定目标注解作为它的值。

容器注解必须有一个名为 value 的元素,其类型为目标注解的数组。这样,当开发者在元素上使用容器注解时,他们可以传递一个目标注解的数组作为 value 的值。

举个栗子:

先定义容器注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {
    MyAnnotation[] value();
}
1
2
3
4
5

容器注解中有一个目标注解的数组 value

然后定义目标注解 @MyAnnotation,和之前一样,只是添加 @Repeatable 元注解,指定了容器注解 @MyAnnotations

@Repeatable(MyAnnotations.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
1
2
3
4
5
6

使用注解,可以同时在一个类上重复使用 MyAnnotation 注解:

@MyAnnotation(value="逗比")
@MyAnnotation(value="牛逼")
class Student {

}
1
2
3
4
5

说了这么多,@Repeatable 用的倒不多。

# 3.5 注解作用初体验

好了,我们现在已经有注解了,注解上也添加了元注解了:

import java.lang.annotation.*;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
1
2
3
4
5
6
7

那么现在这个注解有什么作用呢?

仍然是卵用没有,这里需要解释一下。这个注解就像一个标签,比方说,提到岳飞,你心里可能会给他贴上标签 忠臣 ,提到秦桧,你心里就会给他贴上标签 奸臣 ,但是你给他们贴上标签对他们没有任何影响,所以你在一个类上添加了一个你自定义的标签,对这个类也没有任何影响。

但是哪天如果有一个任务,给历史上的名臣打分,你看到岳飞有一个 忠臣 的标签,于是加10分;秦桧有一个 奸臣 的标签,于是减10分;又过了几天,你又有一个任务,写关于名臣的作文,对带 忠臣 标签的名臣进行了表扬,对带 奸臣 标签的名臣进行了批判。

所以上面的标签有什么用?可以看到它有什么用,关键在于你想怎么用,要做什么。对于注解也是一样的,给类添加了注解,本身没有起什么作用,但是你可以读取到类,看类上如果有这个注解,就可以做一些工作了。


举个栗子:

在进行一些项目开发的时候,我们可能会将一些配置保存到配置文件中,例如 application.properties 中,然后在代码中读取这些配置进行使用。在 SpringBoot 框架中就有这样的功能,下面我们简单模拟一下这个功能。


项目是 Maven 构建的,所以首先在 resources 下创建 application.properties,内容如下:

doubibiji.name=逗比笔记
doubibiji.domain=www.doubibiji.com
1
2

然后定义注解,为了见名知意,起名字叫 PropertyInit

package com.doubibiji.annotation;

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PropertyInit {
    String value();
}
1
2
3
4
5
6
7
8
9
10
11
12

然后在一个类中的属性上使用这个注解,并使用注解的属性指定配置文件中的哪个配置项:

package com.doubibiji.annotation;

public class MyComponent {

    @PropertyInit("doubibiji.name")
    private String name;

    @PropertyInit("doubibiji.domain")
    private String domain;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDomain() {
        return domain;
    }

    public void setDomain(String domain) {
        this.domain = domain;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

好了,下面开始使用代码,读取配置文件中的配置,通过反射为 MyComponent 类的对象赋值。

package com.doubibiji.annotation;

import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Properties;

public class PropertyInitializer {

    private static Properties properties;

    public static void main(String[] args) {

        // 1.首先从配置文件中读取配置
        readProperties();

        MyComponent component = new MyComponent();
        // 2. 利用反射和注解初始化对象中的属性
        initializeFields(component);

        System.out.println("name:" + component.getName());  // name:逗比笔记
        System.out.println("domain:" + component.getDomain());  // domain:www.doubibiji.com
    }

    /**
     * 读取配置文件
     */
    public static void readProperties() {
        InputStream input = null;
        try {
            // 读取application.properties配置文件的内容
            input = PropertyInitializer.class.getClassLoader().getResourceAsStream("application.properties");
            properties = new Properties();
            // 读取配置到 Properties 中
            properties.load(input);
        } catch(Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != input) {
                    input.close();
                }
            } catch(Exception e) {
                e.printStackTrace();
            }
        }
    }

    public static void initializeFields(Object object) {
        // 获取类中的属性
        Field[] fields = object.getClass().getDeclaredFields();

        for (Field field : fields) {
            // 遍历属性,查看属性是否有PropertyInit注解
            if (field.isAnnotationPresent(PropertyInit.class)) {
                // 属性有PropertyInit注解,就获取注解的值
                PropertyInit annotation = field.getAnnotation(PropertyInit.class);
                // 获取到注解的值,也就是配置文件中的key
                String key = annotation.value();
                // 通过key读取配置文件中的value值
                String value = properties.getProperty(key);

                if (value != null) {
                    try {
                        // 设置私有属性可以访问
                        field.setAccessible(true);
                        // 如果属性是字符串类型,就将从配置文件中读取到的值复制给属性
                        if (field.getType() == String.class) {
                            // 设置属性的值
                            field.set(object, value);
                        } else {
                            throw new RuntimeException("不支持的初始化类型:" + PropertyInit.class.getSimpleName());
                        }
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException("初始化属性出错:" + PropertyInit.class.getSimpleName(), e);
                    }
                } else {
                    throw new RuntimeException("配置文件中没有找到key:" + key);
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

上面的代码首先读取配置文件中的配置,然后通过反射获取对象中的属性,然后为对象中 配置了注解 的属性进行赋值,没有配置的属性忽略了。

上面项目的结构:


如果中文有乱码问题,可以设置一下文件的字符编码:

上面只是简单的介绍了通过反射操作注解,关于反射的更多功能,后面再讲。

另外上面实现的功能,在 SpringBoot 项目开发中 SpringBoot 框架已经帮我们实现了这样的功能,不需要我们自己实现。但是通过这个功能也能窥见注解在实际使用过程中的作用。