《回炉重造》——注解
注解
前言
以前学习到「注解」的时候,没有好好理解注解是如何工作的,只是知道注解可以实现一些功能,总而言之,就是懵懵懂懂。
不过,即使你不知道什么是注解,但肯定接触过注解,比如方法的重写,在方法上面写着 @Override
,这个东西就是注解。
好了,下面就开始回炉重造!打好基础!
什么是注解?
注解(Annotation),Annotation 的意思有「标注、批注、附注」。
注解和泛型一样,也是从 JDK5 引入的新特性。它是与类、接口处在同一级别的,有它的语法来定义注解的。
注解,人们还有另一个词来称呼它,即「元数据」,所谓元数据(metadata),就是用来描述数据的数据(data about data)。初看这句话可能会有点懵,不过没关系,都是这样过来的,好好细品细品。
品不过来?可以看看这里:
好了,我们知道「注解==元数据」,那么它描述什么数据呢?问得好,描述的就是我们写的源代码!可以描述类、方法、局部变量、方法参数等数据(这些数据也有另一个叫法,Java Element)。注意上面我说过 Annotation 的意思有 附注 的意思,是吧,所以说这个注解实际上并不是程序本身,只是向编译器提供相关的程序的附加信息,也就是说注解并不会影响程序的运行。
为什么会有注解的出现?
这就扯到历史的发展了,综合我所了解的,大概就是这样子:
这里也扯到了 XML(Extensible Markup Language),如果不熟悉,我这里就简单说下,熟悉可以跳过啦!
XML 翻译过来就是「可扩展的标记语言」,与 HTML 类似。但是 XML 是被设计用来传输数据的,并不是显示数据,具有「自我描述性」。
下面是 Jayson Tatum 写给 Kobe Bryant 的短信,存储为 XML:
<text-message>
<to>Kobe Bryant</to>
<from>Jayson Tatum</from>
<body>I got you today</body>
</text-message>
上面的短信就具有自我描述性。它拥有留言,同时包含了发送者和接受者的信息。当然,这仅仅是描述而已,XML 并没有做任何事情,就是纯文本,我们需要写代码,让代码识别这些标签,赋予意义,这样程序才能读懂 XML,知道它描述的是什么。
目前 XML 有两个作用,一是可以用 XML 格式来传输数据,二是可以作为配置文件。
在注解出现之前,开发者基本是用 XML 来配置某些东西,因为 XML 和代码是低耦合的,符合低耦合的需求,但是,随着从 XML 描述的数据越来越多,配置越来越复杂,人们发现用 XML 描述数据太复杂了,需要一种高耦合的来降低这种复杂的情况,所以才出现注解,用注解来描述数据。
不过,现在也是看情况,并不是说注解代替了 XML,而是有时使用 XML,有时使用注解,各有各的好处,两者相互结合等,这还需要具体情况具体分析。
为什么需要学习注解?
- 很显然,我们现在日常开发,使用到各种开源框架,经常会用到注解,不学习注解,那自然看不懂注解相关的代码。
- 看起来比较厉害
JDK 自带的注解
-
@Override
:表示当前方法覆盖了父类的方法 -
@Deprecated
: 表示该方法由于安全、性能问题等,已经不推荐使用了,即已经过时,方法上有横线,使用时会有警告。此外,在版本升级时,如果要计划删除一些方法,也通常会在前一个版本中,将该方法加上@Deprecated,然后再在后续版本中删除。 -
@SuppviseWarnings(value="unchecked")
: 表示镇压警告信息(让编译器忽略特定的编译警告)一些
value
值:unchecked
,deprecation
(忽略 一些过期的 API 警告)unused
(忽略 没有被使用过的代码的警告)fallthrough
( 忽略 switch 中缺失 break 的警告)all
(忽略全部)
在 JDK 7 和 8,分别加入了 @SafeVarargs
和 @FunctionalInterface
这两个注解。这两个后续再来填坑吧。
@Override有什么用?不加行不行?
@Override
是我们最早接触的注解了,不加它行不行?
行!不加也行!只要重写了方法正确就可以了,要是写错了,举个例子:
public class Pet {
public void eat() {
System.out.println("吃...");
}
}
public class Cat extends Pet{
public void aet() {
System.out.println("吃鱼...");
}
}
这里 Cat 继承了 Pet,重写 eat()
方法,但是方法名写错了,写成 aet()
。
Pet pet = new Cat();
pet.eat(); // 父类引用调用子类方法
由于子类重写的方法写错方法名,那么此时父类引用调用子类方法时,就找不到子类的 eat()
这个方法,然后就只能回去父类找,这显然不是我们想要的。
所以加上 @Override
有好处,写错方法名会有提示,也就是说,可以确保重写的方法,的确存在于父类/接口中,可以有效的避免单词拼错等情况。
注解的分类
有两种分类方式:
- 按照运行机制分类
- 按照来源分类
按照运行机制分类(何时保留,何时不保留,某个时期保留,某个时期不保留,生命周期,Retention)
-
SOURCE——源码注解:注解只在源码中存在,编译成.class文件就不存在了
-
CLASS——编译时注解:注解在源码和
.class文件
中都存在(如:JDK 自带注解) -
RUNTIME——运行时注解:在运行阶段还起作用,甚至会影响运行逻辑的注解(如:Spring中
@Autowired
)
按照来源分类
-
元注解:给注解进行注解,也就是来修饰注解的,有4个;这里的元注解就好比上面说过的元数据,如果还是不能很好的理解的话,就把它理解成形容词;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Target
:用于声明注解的作用域,可以是ElementType.CONSTRUCTOR
可作用于构造方法ElementType.FIELD
字段声明ElementType.LOCAL_VARIABLE
局部变量声明ElementType.METHOD
方法声明ElementType.PACKAGE
包声明ElementType.PARAMETER
参数声明ElementType.TYPE
类、接口、枚举、注解声明。
@Retention
:用于声明注解何时保留,也有人们称为生命周期,可以是RetentionPolicy.SOURCE
只在源码显示,编译时会丢弃RetentionPolicy.CLASS
编译时会记录到class中,运行时忽略RetentionPolicy.RUNTIME
运行时存在,可以通过反射读取。
@Inherited
:允许子类继承,即声明该注解会被使用了该注解的类的子类所继承。@Documented
:使用了这个注解,那么在生成 Javadoc 的时候会包含注解信息。所以,使用这4个元注解,就是用来修饰我们自定义的注解的,规定我们自定义的注解在哪些地方能用,什么时候会保留注解信息等等。
在 JDK 8 新加了一个元注解
@Repeatable
,这个也后续再来填坑啦。 -
JDK 自带的注解
-
常见第三方注解(Spring、MyBatis等等)
-
自定义注解
如何自定义注解
使用 @interface
关键字进行注解的自定义,写出你自己定义的注解~
public @interface 注解名 {
成员变量...以无参数无异常的方式来声明
可以用defalt关键字来指定默认值
}
举个例子:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface TextMessage { // 使用 @interface 关键字定义注解,注解名为 TextMessage
String to(); // 成员变量以无参数无异常的方式来声明
String from() default "Jayson Tatum"; // 可以用defalt关键字来指定默认值
String body();
}
注意事项:
- 注解只有成员变量,没有方法,虽然这个变量有小括号,看起来挺像方法的。
- 注解中定义成员变量时它的类型只能是 8 种基本数据类型外加 String 、类、接口、注解、枚举及它们的数组。
- 若注解只有一个成员变量,则该成员名必须取名为
value()
,然后使用注解的时候参数直接写。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface A {
String value();
}
public class B {
@A("god23bin")
public int var;
}
- 注解可以没有成员变量,这样的注解称为标识注解。
获取注解信息
还记得开头我说过的话吗?
注解实际上并不是程序本身,只是向编译器提供相关的程序的附加信息,也就是说注解并不会影响程序的运行。
是的,注解这样子,根本不能帮我们做什么事情,顶多是给我们人看的,我们知道它描述什么,此时它就和注释的功能很像了,但是!我们知道,注解不仅仅是给我们人看的,也是给机器看的啊,那么机器是如何看的呢?这就涉及到反射了,需要同个反射去获取注解的信息,进而进行下一步的操作,实现我们想要的功能!
举个例子:
我这里自定义了一个注解 B,作用域为类、接口、枚举、注解、方法、字段(成员变量)。
注解的保留时期(Retention)是运行时(RetentionPolicy.RUNTIME
),这是必须的,毕竟反射是运行时动态获取的,不选这个,就不能使用反射获取注解信息了。
该注解有两个成员变量,或者说两个属性,即 id
和 text
,id
默认 -1
。
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface B {
int id() default "-1";
String text();
}
现在,我们把这个注解声明到 Test 类上:
@B()
public class Test {
}
那如何使用反射获取呢?我们可以通过反射中的 Class 对象
的 isAnnotationPresent()
方法判断该类是否使用了某个注解。
/**
* {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @since 1.5
*/
@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return GenericDeclaration.super.isAnnotationPresent(annotationClass);
}
然后通过它的 getAnnotation()
方法来获取 Annotation 对象
。
/**
* @throws NullPointerException {@inheritDoc}
* @since 1.5
*/
@SuppressWarnings("unchecked")
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);
return (A) annotationData().annotations.get(annotationClass);
}
或者是 getAnnotations()
方法来获取 Annotation 数组对象
。
/**
* @since 1.5
*/
public Annotation[] getAnnotations() {
return AnnotationParser.toArray(annotationData().annotations);
}
所以,我们可以这样获取注解信息:
@B("god23bin")
public class Test {
public static void main(String[] args) {
boolean isB = Test.class.isAnnotationPresent(B.class);
if (isB) {
B b = Test.class.getAnnotation(B.class);
System.out.println("id:" + b.id());
System.out.println("text:" + b.text());
}
}
}
输出结果:
id:-1
text:god23bin
同理,位于接口、属性(字段)、方法上的注解,同样可以通过反射获取。
@B(text="I got you today")
public class Test {
@B(id=1, text="hello")
public int var;
@B(text="world")
public void empty() {
}
public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException {
boolean isB = Test.class.isAnnotationPresent(B.class);
if (isB) {
// 获取类上的注解
B b1 = Test.class.getAnnotation(B.class);
System.out.println("1-id:" + b1.id());
System.out.println("1-text:" + b1.text());
}
Field field = Test.class.getDeclaredField("var");
field.setAccessible(true);
// 获取字段上的注解
B b2 = field.getAnnotation(B.class);
if (b2 != null) {
System.out.println("2-id:" + b2.id());
System.out.println("2-text:" + b2.text());
}
// 获取方法上的注解
Method method = Test.class.getDeclaredMethod("empty");
B b3 = method.getAnnotation(B.class);
if (b3 != null) {
System.out.println("3-id:" + b3.id());
System.out.println("3-text:" + b3.text());
}
}
}
输出结果:
1-id:-1
1-text:I got you today
2-id:1
2-text:hello
3-id:-1
3-text:world
好了,到这里,上面已经知道我们可以通过反射获取注解信息,但是目前仅仅只是获取信息而已,如何实现其他功能呢?这个问题问的好,后续再来填坑吧,不过还是一样,离不开反射。
以上就是注解的基本内容了。
最后的最后
由本人水平所限,难免有错误以及不足之处, 屏幕前的靓仔靓女们
如有发现,恳请指出!
最后,谢谢你看到这里,谢谢你认真对待我的努力,希望这篇博客对你有所帮助!
你轻轻地点了个赞,那将在我的心里世界增添一颗明亮而耀眼的星!