《回炉重造》——注解

注解

前言

以前学习到「注解」的时候,没有好好理解注解是如何工作的,只是知道注解可以实现一些功能,总而言之,就是懵懵懂懂。

不过,即使你不知道什么是注解,但肯定接触过注解,比如方法的重写,在方法上面写着 @Override,这个东西就是注解。

好了,下面就开始回炉重造!打好基础!

注解

什么是注解?

注解(Annotation),Annotation 的意思有「标注、批注、附注」。

注解和泛型一样,也是从 JDK5 引入的新特性。它是与类、接口处在同一级别的,有它的语法来定义注解的

注解,人们还有另一个词来称呼它,即「元数据」,所谓元数据(metadata),就是用来描述数据的数据(data about data)。初看这句话可能会有点懵,不过没关系,都是这样过来的,好好细品细品。

品不过来?可以看看这里:

什么是元数据?为何需要元数据? - 贺易之的回答 - 知乎

元数据(MetaData)- 阮一峰

好了,我们知道「注解==元数据」,那么它描述什么数据呢?问得好,描述的就是我们写的源代码!可以描述类、方法、局部变量、方法参数等数据(这些数据也有另一个叫法,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,有时使用注解,各有各的好处,两者相互结合等,这还需要具体情况具体分析。

为什么需要学习注解?

  1. 很显然,我们现在日常开发,使用到各种开源框架,经常会用到注解,不学习注解,那自然看不懂注解相关的代码。
  2. 看起来比较厉害

JDK 自带的注解

  • @Override :表示当前方法覆盖了父类的方法

  • @Deprecated: 表示该方法由于安全、性能问题等,已经不推荐使用了,即已经过时,方法上有横线,使用时会有警告。此外,在版本升级时,如果要计划删除一些方法,也通常会在前一个版本中,将该方法加上@Deprecated,然后再在后续版本中删除。

  • @SuppviseWarnings(value="unchecked"): 表示镇压警告信息(让编译器忽略特定的编译警告)

    一些 value 值:

    • uncheckeddeprecation(忽略 一些过期的 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),这是必须的,毕竟反射是运行时动态获取的,不选这个,就不能使用反射获取注解信息了。

该注解有两个成员变量,或者说两个属性,即 idtextid 默认 -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

好了,到这里,上面已经知道我们可以通过反射获取注解信息,但是目前仅仅只是获取信息而已,如何实现其他功能呢?这个问题问的好,后续再来填坑吧,不过还是一样,离不开反射。

以上就是注解的基本内容了。

最后的最后

由本人水平所限,难免有错误以及不足之处, 屏幕前的靓仔靓女们 如有发现,恳请指出!

最后,谢谢你看到这里,谢谢你认真对待我的努力,希望这篇博客对你有所帮助!

你轻轻地点了个赞,那将在我的心里世界增添一颗明亮而耀眼的星!

posted @ 2022-06-03 20:27  god23bin  阅读(713)  评论(0编辑  收藏  举报