深入理解Java中的注解Annotation
作者:发光吖
Java中的注解(Annotation)
单词Annotation可翻译为:注释、注解。
单词Comment可翻译为:评论、议论、解释。
在Java当中,Comment充当注释的含义,Annotation充当注解的含义。
注解和注释有啥区别呢?
Java中有单行注释、多行注释、文档注释。
注释Comment
- 单行注释 //
- 多行注释 /* */
- 文档注释 /** */
这些注释都是在编译以后不会出现在字节码中的,仅仅是存在于java源文件中给程序员看的,它们对于程序的执行没有任何影响。
而注解就不同了,它是代码级别的,是会编译到class字节码当中,对程序的运行产生影响或者对编译产生影响。
注解严格意义上来说就是Java中的类成员,它和属性、方法、构造方法是一样的级别。
初学注解
注解在Java中确实也很常见,但是人们常常不会自己定义一个注解拿来用。我们虽然很少去自定义注解,但是学会注解的写法,注解的定义,学会利用反射解析注解中的信息,在开发中能够使用到,这是很关键的。
代码运行离不开一些配置信息的支撑,有些配置信息我们选择保存在文件中(.xml .properties),再利用IO流的技术读取这些配置信息去使用,它们都是配置和代码分离的形式,这种方式的好处是低耦合,代码已经打包压缩好了不用动,而配置信息修改起来很方便,通过修改配置,可以让代码完成不同的任务,这就很方便。不好的地方在于:开发人员写的代码和配置不在一个文件中,开发过程中翻看就不是很方便。相当于开发麻烦了,维护却简单了。
随着开发的越来越多,人们发现文件中的一些配置,是写到那里很少去更改的,没有人会轻易修改那些重要的配置信息,这些信息就像被写死了一样,所以这些信息就可以用注解写,写到代码里去,因为注解和代码是内聚在一起的,开发过程就很方便。总结来说是各有优劣吧。
1.注解的写法
@XXX [(一些信息)]
这是使用注解的写法,使用注解前必须先定义注解,我们可以使用像@Override这样的注解,那是因为这些注解在Java中已经写好了,我们直接拿来用就好(下面会讲到如何自定义注解)。
注解中的[一些信息]可能存在,也可能不存在,这需要看注解的定义者是如何定义该注解的。
2.注解放置在哪里
注解可以放置在:类的上面、属性上面、方法上面、构造方法上面、局部变量上面、参数前面。
注解能够放置在哪里,这也需要看注解的定义者是如何定义该注解的。
3.注解的作用
- 用来充当注释的作用(仅仅是一个文字的说明) 例如:@Deprecated
- 用来做代码的检测(验证) 例如:@Override
- 可以携带一些信息(内容) 就类似于:文件.properties .xml 的作用
4.Java中有一些写好的注解供我们使用
- @Deprecated 用来说明方法是废弃的
- @Override 用来做代码检测,检测此方法是否是一个重写方法,如果不是,该注解会报错
- @SuppressWarnings({"","",""}) 它里面的参数是一个String类型的数组,如果数组内的元素只有一个长度 ,则大括号{}可以省略,该注解中可以定义的有意义参数值包括:
- unused:变量定义后未被使用
- serial:类实现了序列化接口 不添加序列化ID号
- rawtypes:集合没有定义泛型
- deprecation:方法已经废弃
- *unchecked:出现了泛型的问题 可以不检测
- all:包含了以上所有(不推荐)
5.注解中可以携带信息,也可以不携带
注意:注解信息不能随意写,注解信息的类型只能是如下的类型:
- 基本数据类型
- String类型
- 枚举类型enum
- 注解类型@
- 数组类型[],数组的内部只能存储如上的四种类型
基本数据类型String类型枚举类型enum注解类型@数组类型[],数组的内部只能存储如上的四种类型
自定义一个注解类型
通过@interface 定义一个新的注解类型
public @interface MyAnnatation {<!--{C}%3C!%2D%2D%20%2D%2D%3E-->}- 可以发现注解的写法与接口非常相似(可以利用接口的特点来记忆注解)
- 可以描述public static final的属性 比较少见可以描述public abstract的方法 方法要求必须有返回值 返回值类型必须是如上那些(基本数据类型、String类型、枚举类型、注解类型、数组类型)
public @interface MyAnnatation {
int NUM = 9;//注解中写属性,很少见
String test();//方法要求必须有返回值
}
注解元素的默认值
注解元素必须有确定的值,要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。因此, 使用空字符串或0作为默认值是一种常用的做法。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为每个注解的声明中,所有元素都存在,并且都具有相应的值,为了绕开这个约束,我们只能定义一些特殊的值,例如空字符串或者负数,一次表示某个元素不存在,在定义注解时,这已经成为一个习惯用法。
元注解
我们自己定义的注解如果想要拿来使用,光定义还不够 ,还需要做很多细致的说明(需要利用Java提供好的注解来说明)
这就需要使用到元注解(也是注解 不是拿来使用的 是用来说明注解的):
@Target
@Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。在Annotation类型的声明中使用了target可更加明晰其修饰的目标。
作用:
用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
取值(ElementType)有:
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域
- LOCAL_VARIABLE:用于描述局部变量
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述参数
- TYPE:用于描述类、接口(包括注解类型) 或enum声明
更详细的@Target定义可参照下表:
| Target | 类型描述 |
| ElementType.TYPE | 应用于类、接口(包括注解类型)、枚举 |
| ElementType.FIELD | 应用于属性(包括枚举中的常量) |
| ElementType.METHOD | 应用于方法 |
| ElementType.PARAMETER | 应用于方法的形参 |
| ElementType.CONSTRUCTOR | 应用于构造函数 |
| ElementType.LOCAL_VARIABLE | 应用于局部变量 |
| ElementType.ANNOTATION_TYPE | 应用于注解类型 |
| ElementType.PACKAGE | 应用于包 |
| ElementType.TYPE_PARAMETER | 1.8版本新增,应用于类型变量) |
| ElementType.TYPE_USE | 1.8版本新增,应用于任何使用类型的语句中(例如声明语句、泛型和强制转换语句中的类型) |
@Retention
- 描述当前的这个注解存在什么作用域中
- 注解存在的三种作用域:SOURCE、CLASS、RUNTIME
- 对应于:源代码文件—>编译—>字节码文件—>加载—>内存执行
@Retention定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
取值(RetentionPoicy)有:
- SOURCE:在源文件中有效(即源文件保留)
- CLASS:在class文件中有效(即class保留)
- RUNTIME:在运行时有效(即运行时保留)
| 生命周期类型 | 描述 |
| RetentionPolicy.SOURCE | 编译时被丢弃,不包含在类文件中 |
| RetentionPolicy.CLASS | JVM加载时被丢弃,包含在类文件中,默认值 |
| RetentionPolicy.RUNTIME | 由JVM 加载,包含在类文件中,在运行时可以被获取到 |
Retention meta-annotation类型有唯一的value作为成员,它的取值来自java.lang.annotation.RetentionPolicy的枚举类型值。
@Inherited
描述当前这个注解是否能被子类对象继承(不太常用)
@Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类。
注意:@Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation。
当@Inherited annotation类型标注的annotation的Retention是RetentionPolicy.RUNTIME,则反射API增强了这种继承性。如果我们使用java.lang.reflect去查询一个@Inherited annotation类型的annotation时,反射代码检查将展开工作:检查class和其父类,直到发现指定的annotation类型被发现,或者到达类继承结构的顶层。
@Document
描述这个注解是否能被Javadoc 或类似的工具文档化(不常用)
自己使用自己描述的注解
自定义一个注解:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
@Target({METHOD,CONSTRUCTOR,FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String[] value();//方法不是做事情 为了携带信息 搬运给该注解的解析者使用
//按道理讲 注解定义者肯定和注解解析者是同一个人,而注解的使用者,它们无需定义和解析注解
/*方法名刚好是value,并且只有一个方法,使用该注解的时候就可以不指定方法名*/
}
使用自己的注解:
由注解的定义可知,该注解可以放置在方法、构造方法、属性上。
作用范围是运行时RUNTIME
public class Person {
@MyAnnotation("TOM")
private String name;
}
问题1. 在注解里面描述了一个方法,方法没有参数,方法有返回值String[]
使用注解的时候,让我们传递参数,如何理解该过程?
可以理解为:注解的方法做事,把我们传递给它的参数搬运走了,给了别人,别人解析这些注解中的参数,做相应的处理
问题2. 使用别人写好的注解不用写方法名,我们自己定义的方法必须写名字*
- 如果我们自己定义的注解 只有一个方法 方法名字叫value
- 在使用的时候就可以省略方法名
- 如果传递的信息是一个数组 数组内只有一个元素 可以省略数组大括号{}
- 如果方法是两个以上 每一个方法都要使用 并且每一个方法必须写名字
举例:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
@Target({METHOD,FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface YouAnnotation {
String [] value();
int count();
double price();
}
public class Student {
//注解中有3个方法,则3个方法都要使用到,并且要指明方法名
//如果数组中只有一个数据,则{}可以省略
@YouAnnotation(value = "qa",count = 8,price = 9.9)
private String name;
}
如何解析注解内携带的信息(反射机制)
解析注解其实也很简单,它的思路是:先看该注解声明在了哪里,比如,一个属性name上面声明了一个注解,那么就利用反射,先找到这个类,然后找到这个属性name,根据这个属性,调用getAnnotation()方法,得到这个注解,然后调用这个注解对象的getClass方法获取注解的类型,用注解的类型,调用getMethod(methodName)方法,根据方法名获取注解中的方法,然后,这个方法对象调用 invoke方法去执行方法,方法的参数是那个annotation对象,方法的返回值就是这个注解中携带的信息了。
public class Demo {
public static void main(String[] args) {
Class<?> clazz = Person.class;
try {
Field field = clazz.getDeclaredField("name");
MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
Class<? extends MyAnnotation> aClass = annotation.getClass();
Method method = aClass.getMethod("value");
String[] values = (String[])method.invoke(annotation);
System.out.println(Arrays.toString(values));
} catch (Exception e) {
e.printStackTrace();
}
}
}
做个小案例,感受一下控制反转和依赖注入的基本原理
Spring的核心特性是控制反转(IOC)和面向切面编程(AOP)。控制反转是指对象的控制权不在我们手里了,而是交给了Spring给我们创建。我们只需要把实体类定义好,提供好无参构造方法和get、set方法就好了,它给你的对象自动赋值了,这就叫依赖注入(DI)。该案例它只能够处理9种属性类型,包括8种基本类型的包装类以及String,其他的类型还不能支持。
package test_annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
/**
* @author 乔澳
* @version 1.0
* @title: MySpringDemo
* @projectName Demo1
* @description:
* @date 2020/8/17 18:22
*/
public class MySpringDemo {
public Object getBean(String className){
Object obj = null;
Class<?> clazz = null;
try {
clazz = Class.forName(className);
//获取无参构造方法
Constructor con = clazz.getConstructor();
//调用无参构造方法创建对象
obj = con.newInstance();
//解析注解中的信息
//注解放在属性上面,首先获取所有的属性
Field[] fields = clazz.getDeclaredFields();
for(int i = 0;i<fields.length;i++){
//根据属性获取属性上面声明的注解
Annotation annotation = fields[i].getAnnotation(MyAnnotation.class);
Class<?> aClass = annotation.getClass();
Method aMethod = aClass.getMethod("value");
//获取注解中的值
String[] values = (String[]) aMethod.invoke(annotation);
//获取属性名
String fieldName = fields[i].getName();
//要给属性赋值,属性是私有的,虽然反射可以操作私有属性,但是很不合理
//我们利用字符串的拼接,得到set方法的名字,再拿到set方法给属性赋值
String firstLetter = fieldName.substring(0,1).toUpperCase();//首字母大写
String otherLetters = fieldName.substring(1);
StringBuilder setMethodName = new StringBuilder("set");
setMethodName.append(firstLetter);
setMethodName.append(otherLetters);
//拿到属性的类型,下面要用到
Class<?> fieldType = fields[i].getType();
//根据set方法名字拿到set方法
Method setMethod = clazz.getMethod(setMethodName.toString(),fieldType);
//调用set方法,给对象赋值,如果属性不是String类型,是基本类型的包装类
//如果属性是Character类型,做单独处理
if (fieldType==Character.class){
//把字符串转化为字符
Character c = values[0].toCharArray()[0];
setMethod.invoke(obj,c);
}else{
//不是Character类型,而是String 或其他7种包装类,调用包装类的带String参数的构造方法,构造值 比如;new Integer(String v)
setMethod.invoke(obj,fieldType.getConstructor(String.class).newInstance(values[0]));
}
//对于属性为数组、集合、对象的情况,这里没有做处理
}
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
}
测试:
写一个实体类
package test_annotation;
public class Person {
@MyAnnotation("Tom")
private String name;
@MyAnnotation("18")
private Integer age;
@MyAnnotation("男")
private String sex;
@MyAnnotation("A")
private Character bloodType;//血型
public Person(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public Character getBloodType() {
return bloodType;
}
public void setBloodType(Character bloodType) {
this.bloodType = bloodType;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
", bloodType=" + bloodType +
'}';
}
}
main方法:
package test_annotation;
public class TestMain {
public static void main(String[] args) {
MySpringDemo msd = new MySpringDemo();
Person p = (Person) msd.getBean("test_annotation.Person");
System.out.println(p);
}
}
到此这篇关于深入理解Java中的注解Annotation的文章就介绍到这了,更多相关深入理解Java注解内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
