# 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()");
}
}
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
}
}
2
3
4
5
6
7
8
9
10
上面代码看不出区别,在 IDEA 中就会有提示,直接有横杠,提示过期,不建议使用:
# 3.2 自定义注解
上面说的是 JDK 自带的注解,在实际的开发中,自定义注解才能发挥注解的强大作用。在 Java Web开发中,现在用的 SpringMVC、SpringBoot、Mybatis等框架,都是使用注解进行各种配置,没有注解是无法简单的实现这些优秀的框架的,可能需要 xml
配置,非常不方便,可以说 框架 = 注解+反射+设计模式,反射后面再讲,没有反射,注解也发挥不出作用。
下面讲一下如何自定义注解。
# 1 定义注解
举个栗子:
定义一个名称为 MyAnnotation
的注解:
public @interface MyAnnotation {
}
2
3
语法和定义接口十分相似,在 interface
前面加 @
,但是注解和接口没有什么关系,只是自定义注解自动继承了**java.lang.annotation.Annotation
** 接口。
# 2 添加属性
注解中也是可以添加属性的,不过和类中的属性有些区别,使用无参数方法的形式来定义。
举个栗子:
public @interface MyAnnotation {
String value();
int index();
}
2
3
4
上面的注解中,定义了两个属性,value
和 index
。如果只有一个参数成员,推荐属性名使用 value
。属性的类型支持8种基本数据类型(int
, boolean
, char
, long
, double
, float
, byte
, short
以及它们的包装类),String
,Class
,枚举类型,注解类型,以及数组上述类型的数组。
也可以为属性设置默认值:
public @interface MyAnnotation {
String value() default "www.doubibiji.com";
int index();
}
2
3
4
上面为 value 设置了默认值为 www.doubibiji.com
。
# 3.3 使用注解
上面定义了注解,现在可以使用注解了。
定义一个 Student 类,在类上使用 MyAnnotation
注解。
@MyAnnotation(value="doubi", index=0)
class Student{
}
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();
}
2
3
4
5
6
7
什么意思呢,当我们使用 @MyAnnotation
注解的时候,指定了 @MyAnnotation
注解的保存策略。
@MyAnnotation("doubi")
class Student{
}
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();
}
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();
}
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_PARAMETER
和 ElementType.TYPE_USE
再举个栗子:
如果 @MyAnnotation
注解的 target 添加 ElementType.TYPE_PARAMETER
,那么可以修饰泛型:
class Student<@MyAnnotation("doubi") T>{
}
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;
}
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) {
}
}
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();
}
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();
}
2
3
4
5
6
7
8
然后使用 MyAnnotation
注解修饰 Person
类:
@MyAnnotation("doubi")
class Person {
}
class Student extends Person{
}
2
3
4
5
6
7
8
因为 Student
类继承了 Person
类,Person
类使用了 @MyAnnotation
注解,所以 Student
类也拥有了 @MyAnnotation
注解。
# 5 @Repeatable
@Repeatable
是 Java 8 中引入的一个元注解,它允许在同一个元素(如类、方法或字段)上重复使用相同的注解。在 @Repeatable
出现之前,Java 的注解是不支持在同一个元素上重复使用的,这限制了注解的灵活性和表达力。
要使用 @Repeatable
注解,你需要定义两个注解:
- 目标注解:这是你想要重复使用的注解。
- 容器注解:这是一个特殊的注解,用于存储目标注解的多个实例。这个容器注解需要使用
@Repeatable
来修饰,并指定目标注解作为它的值。
容器注解必须有一个名为 value
的元素,其类型为目标注解的数组。这样,当开发者在元素上使用容器注解时,他们可以传递一个目标注解的数组作为 value
的值。
举个栗子:
先定义容器注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotations {
MyAnnotation[] value();
}
2
3
4
5
容器注解中有一个目标注解的数组 value
。
然后定义目标注解 @MyAnnotation
,和之前一样,只是添加 @Repeatable
元注解,指定了容器注解 @MyAnnotations
:
@Repeatable(MyAnnotations.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}
2
3
4
5
6
使用注解,可以同时在一个类上重复使用 MyAnnotation
注解:
@MyAnnotation(value="逗比")
@MyAnnotation(value="牛逼")
class Student {
}
2
3
4
5
说了这么多,@Repeatable
用的倒不多。
# 3.5 注解作用初体验
好了,我们现在已经有注解了,注解上也添加了元注解了:
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
}
2
3
4
5
6
7
那么现在这个注解有什么作用呢?
仍然是卵用没有,这里需要解释一下。这个注解就像一个标签,比方说,提到岳飞,你心里可能会给他贴上标签 忠臣
,提到秦桧,你心里就会给他贴上标签 奸臣
,但是你给他们贴上标签对他们没有任何影响,所以你在一个类上添加了一个你自定义的标签,对这个类也没有任何影响。
但是哪天如果有一个任务,给历史上的名臣打分,你看到岳飞有一个 忠臣
的标签,于是加10分;秦桧有一个 奸臣
的标签,于是减10分;又过了几天,你又有一个任务,写关于名臣的作文,对带 忠臣
标签的名臣进行了表扬,对带 奸臣
标签的名臣进行了批判。
所以上面的标签有什么用?可以看到它有什么用,关键在于你想怎么用,要做什么。对于注解也是一样的,给类添加了注解,本身没有起什么作用,但是你可以读取到类,看类上如果有这个注解,就可以做一些工作了。
举个栗子:
在进行一些项目开发的时候,我们可能会将一些配置保存到配置文件中,例如 application.properties
中,然后在代码中读取这些配置进行使用。在 SpringBoot 框架中就有这样的功能,下面我们简单模拟一下这个功能。
项目是 Maven 构建的,所以首先在 resources
下创建 application.properties
,内容如下:
doubibiji.name=逗比笔记
doubibiji.domain=www.doubibiji.com
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();
}
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;
}
}
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);
}
}
}
}
}
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 框架已经帮我们实现了这样的功能,不需要我们自己实现。但是通过这个功能也能窥见注解在实际使用过程中的作用。