【Java】【重要特性】详解注解
一、Java注解是什么
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
注意点:这里说的注释跟我们在代码里面使用//,/**/注释代码是不一样的,普通的代码注释会被编译器给忽略掉,而注解会被编译器打包到class文件中。
二、为什么需要Java注解
从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
那么我们为什么会把注解这个特性加入到Java中呢?
我们在早期没有引入Java注解之前,对我们的程序代码的增加一些配置,一般通过xml配置方式去处理,这样配置和代码隔离开来了所以XML配置更适合做一些全局的、与具体代码无关的操作,例如全局的配置等。但是如果不需要将这些配置跟代码隔离,那我们怎么处理,做一些与代码相关度高的操作,例如将Bean对应的服务暴露出去。这是就出现了注解,使用注解开发速度快,编译期间容易发现错误的出处。
三、如何使用Java注解
(1)注解的分类;Java的注解可以分为三类:
1)第一类是由编译器使用的注解,例如:
@Override:让编译器检查该方法是否正确地实现了覆写;
@SuppressWarnings:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。
2)第二类是由工具处理.class文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
3)第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
(2)注解的作用分类
1)编写文档: 通过代码里标识的元数据生成文档【生成文档doc文档】
我们要知道Java中是有三种注释的,分别为单行注释、多行注释和文档注释。而文档注释中,也有@开头的元注解,这就是基于文档注释的注解。我们可以使用javadoc命令来生成doc文档,此时我们文档的内元注解也会生成对应的文档内容。这就是编写文档的作用
2)代码分析: 通过代码里标识的元数据对代码进行分析【使用反射】
3)编译检查: 通过代码里标识的元数据让编译器能够实现基本的编译检查【Override等】
可以用来做特定的编译检查,它可以在编译期间就检查出“你是否按规定办事”,如果不按照注解规定办事的话,就会在编译期间飘红报错,并予以提示信息。可以就可以为我们代码提供了一种规范制约,避免我们后续在代码中处理太多的代码以及功能的规范。比如,@Override注解是在我们覆盖父类(父接口)方法时出现的,这证明我们覆盖方法是继承于父类(父接口)的方法,如果该方法稍加改变就会报错;@FunctionInterface注解是在编译期检查是否是函数式接口的,如果不遵循它的规范,同样也会报错。
(3)简单使用
Java的内置注解:
- @Override: 标记在成员方法上,用于标识当前方法是重写父类(父接口)方法,编译器在对该方法进行编译时会检查是否符合重写规则,如果不符合,编译报错。
- @Deprecated: 用于标记当前类、成员变量、成员方法或者构造方法过时如果开发者调用了被标记为过时的方法,编译器在编译期进行警告。
- @SuppressWarnings: 压制警告注解,可放置在类和方法上,该注解的作用是阻止编译器发出某些警告信息。
定义一个注解时,还可以定义配置参数。配置参数可以包括:
- 所有基本类型;
- String;
- 枚举类型;
- 基本类型、String、Class以及枚举的数组。
因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。
注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。
此外,大部分注解会有一个名为value的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。如果只写注解,相当于全部使用默认值。
例如:下面@Check就是一个注解。第一个@Check(min=0, max=100, value=55)明确定义了三个参数,第二个@Check(value=99)只定义了一个value参数,它实际上和@Check(99)是完全一样的。最后一个@Check表示所有参数都使用默认值。
public class Hello {
@Check(min=0, max=100, value=55)
public int n;
@Check(value=99)
public int p;
@Check(99) // @Check(value=99)
public int x;
@Check
public int y;
}
(4)如何定义注解
Java语言使用@interface语法来定义注解(Annotation),它的格式如下:
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
注解的参数类似无参数方法,可以用default设定一个默认值(强烈推荐)。最常用的参数应当命名为value。
(5)元注解
有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。
1)@Target元注解:使用@Target可以定义Annotation能够被应用于源码的哪些位置:
- 类或接口:ElementType.TYPE;
- 字段:ElementType.FIELD;
- 方法:ElementType.METHOD;
- 构造方法:ElementType.CONSTRUCTOR;
- 方法参数:ElementType.PARAMETER。
定义注解@Report可用在方法或字段上,可以把@Target注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }:
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}
元注解@Target都有一个value是ElementType[]数组,只有一个元素时,可以省略数组的写法。
2)@Retention元注解:定义了Annotation的生命周期
- 仅编译期:RetentionPolicy.SOURCE;
- 仅class文件:RetentionPolicy.CLASS;
- 运行期:RetentionPolicy.RUNTIME。
如果@Retention不存在,则该Annotation默认为CLASS。
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
3)@Repeatable元注解:可以定义Annotation是否可重复,使用的不多
@Repeatable(Reports.class)
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
经过@Repeatable修饰后,在某个类型声明处,就可以添加多个@Report注解:
@Report(type=1, level="debug")
@Report(type=2, level="warning")
public class Hello {
}
4)@Inherited元注解:定义子类是否可继承父类定义的Annotation。
@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承有效,对interface的继承无效:
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}
在使用的时候,如果一个类用到了@Report:
@Report(type=1)
public class Person {
}
则它的子类默认也定义了该注解:
public class Student extends Person {
}
(6)自定义注解(一般都是使用在RetentionPolicy.RUNTIME)与反射
因为注解定义后也是一种class,所有的注解都继承自java.lang.annotation.Annotation,因此,读取注解,需要使用反射API。
Java提供的使用反射API读取Annotation的方法包括:
1)判断某个注解是否存在于Class、Field、Method或Constructor:
- Class.isAnnotationPresent(Class)
- Field.isAnnotationPresent(Class)
- Method.isAnnotationPresent(Class)
- Constructor.isAnnotationPresent(Class)
2)使用反射API读取Annotation:
- Class.getAnnotation(Class)
- Field.getAnnotation(Class)
- Method.getAnnotation(Class)
- Constructor.getAnnotation(Class)
使用反射API读取Annotation有两种方法。
方法一是先判断Annotation是否存在,如果存在,就直接读取:
Class cls = Person.class;
if (cls.isAnnotationPresent(Report.class)) {
Report report = cls.getAnnotation(Report.class);
...
}
第二种方法是直接读取Annotation,如果Annotation不存在,将返回null:
Class cls = Person.class;
Report report = cls.getAnnotation(Report.class);
if (report != null) {
...
}
(7)使用注解
定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。这里,我们编写一个Person实例的检查方法,它可以检查Person实例的String字段长度是否满足@Range的定义:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
// 遍历所有Field:
for (Field field : person.getClass().getFields()) {
// 获取Field定义的@Range:
Range range = field.getAnnotation(Range.class);
// 如果@Range存在:
if (range != null) {
// 获取Field的值:
Object value = field.get(person);
// 如果值是String:
if (value instanceof String) {
String s = (String) value;
// 判断值是否满足@Range的min/max:
if (s.length() < range.min() || s.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}
这样一来,我们通过@Range注解,配合check()方法,就可以完成Person实例的检查。注意检查逻辑完全是我们自己编写的,JVM不会自动给注解添加任何额外的逻辑。
- 点赞
- 收藏
- 关注作者
评论(0)