秒懂,Java 注解 (Annotation)看这一篇就够了

Annotation 中文译过来就是注解、标释的意思,在 Java 中注解是一个很重要的知识点,但经常还是有点让新手不容易理解。

我们经常可以看到一些文章对注解的解释如下:

Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。

这段对于注解(Annotation)的解释确实正确,但是说实在话,我第一次学习的时候,头脑一片空白。这什么跟什么啊?听了像没有听一样。因为概念太过于抽象,所以初学者实在是比较吃力才能够理解,然后随着自己开发过程中不断地强化练习,才会慢慢对它形成正确的认识。

所以,我理解的注解就是一种标签,想像代码具有生命,注解就是对于代码中某些鲜活个体的贴上去的一张标签,那么注解就是给编译器看的一种标签,通过这个标签,编译器可以快速“联想”到具体的代码

一、注解的语法

熟悉Java Web开发的同学对注解应该不会陌生,Spring家族使用了大量的注解来简化了我们的开发,其实同和class和 interface 一样,注解也属于一种类型。它是在 Java SE 5.0 版本中开始引入的概念。

1、注解的定义

注解通过@interface注解来定义

public @interface MyAnnotation{
    //TODO
}

它的形式跟接口很类似,不过前面多了一个 @符号。上面的代码就创建了一个名字为 MyAnnotation的注解。

你可以简单理解为创建了一张名字为 MyAnnotation的标签。

定义好注解之后,我们就可以使用注解了,最简单的就是在一个类头上使用注解:

@MyAnnotation
public class App{
    //TODO
}

你可以简单理解为将 MyAnnotation这张标签贴到 Test 这个类上面。不过,要想注解能够正常工作,还需要介绍一下一个新的概念那就是元注解

2、元注解

元注解是什么意思呢?

元注解是可以加解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。

如果难于理解的话,你可以这样理解。元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的。

Java提供的元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种。下面详细来讲解一下。

(1)@Rentention

Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。

它的取值如下:

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。

注意:SOURCE和CLASS级别需要继承AbstractProcessor并实现process方法来寻找并处理自定义注解,RUNTIME是我们日常使用最多的,无需继承AbstractProcessor,配合Java反射机制就可以处理自定义注解。

当使用@Rentention标注在一个注解上的时候就表示指定这个注解的工作时间。

@Rentention(RententionPolicy.RUNTIME)
public @interface MyAnnotation{
    //TODO
}

上面的代码中,我们指定 MyAnnotation可以在程序运行周期被获取到,因此它的生命周期非常的长。

(2)@Document

这是一个和文档有关的注解,它的作用是能够将注解中的元素包含到 Javadoc 中去。

(3)@Target

Target 是目标的意思,@Target 指定了注解运用的地方。当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。@Target 有下面的取值:

  • ElementType.ANNOTATION_TYPE:可以在注解上使用
  • ElementType.CONSTRUCTOR:可以在构造器上使用
  • ElementType.FIELD:可以在属性上使用
  • ElementType.LOCAL_VARIABLE:可以在局部变量上使用
  • ElementType.METHOD:可以在方法上使用
  • ElementType.PACKAGE:可以在包名上使用
  • ElementType.PARAMETER:可以在一个方法的形参上使用
  • ElementType.TYPE:可以在类、接口、枚举类等头上使用
(4)@Inherited

Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。

@Inherited
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    //TODO
}

---------------------------------------------------------
@MyAnnotation
public class SuperClass{
    //TODO
}

public class SubClass extends SuperClass{
    //TODO
}

注解 MyAnnotation被 @Inherited 修饰,之后类 SuperClass被 MyAnnotation注解,类 SubClass 继承 SuperClass,虽然没有在类SubClass上标注,但是类 SubClass 也拥有 MyAnnotation这个注解。这是@Inherited注解的含义。

(5)@Repeatable

Repeatable 自然是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。他的含义是说明这个注解可以在同一个Element上多次标记。

public @interface Persons {
	Person[]  value();
}

@Repeatable(Persons.class)
public @interface Person {
     String role() default "";
}

--------------------------------------------------------------
@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{
	
}

在Person注解上使用@Repeatable之后,@Person就可以在一个类上多次使用。

好了,关于Java中的5个元注解就说到这,接下来,我们在来了解一下注解的属性如果定义。

3、注解的属性

注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。比如:

@Inherited
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
    int id();
}

上面就定义了给MyAnnotation这个注解定义了两个属性:value和key。在定义的时候我们还可以使用default关键字给属性指定默认值:

@Inherited
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value()  default "";
    int id() default 0;
}

上面给两个属性有指定了默认值,在使用的时候我们也可以给属性指定一个值,多个属性用逗号(,)隔开:

@MyAnnotation(value="Java",id=1)
public class TestClass{
    //TODO
}

另外在使用注解的时候,如果注解中只有一个属性,那么可以不写属性名直接写值也可以,如果注解中没有任何属性,那么直接使用注解标注就可以了。

二、Java中预置的重要注解

学习了上面相关的知识,我们已经可以自己定义一个注解了。其实 Java 语言本身已经提供了几个现成的注解。

(1)@Deprecated

这个是用来标记过期元素的,他几乎何以作用在任何元素上。如果在我们的代码中使用额被@Deprecated标记的API,那么在编译阶段编译器就会发出警告,告诉开发者正在调用一个过时的元素比如过时的方法、过时的类、过时的成员变量。

(2)@Override

这个大家应该很熟悉了,提示子类要复写父类中被 @Override 修饰的方法

(3)@SuppressWarnings

阻止警告的意思。之前说过调用被 @Deprecated 注解的方法后,编译器会警告提醒,而有时候开发者会忽略这种警告,他们可以在调用的地方通过 @SuppressWarnings 达到目的。

@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan {

    public void coding(){

    }

    @SuppressWarnings(value = "deprecation")
    @Deprecated
    public void speak(){
    
    }

    public void drawing(){

    }

}
(4)@SafeVarargs

参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。它是在 Java 1.7 的版本中加入的。

(5)@FunctionalInterface

函数式接口注解,这个是 Java 1.8 版本引入的新特性。函数式编程很火,所以 Java 8 也及时添加了这个特性。

函数式接口 (Functional Interface) 就是一个只有一个抽象方法的接口(注意是只有一个抽象方法,不是只有一个方法)。比如:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

我们进行线程开发中常用的 Runnable 就是一个典型的函数式接口,上面源码可以看到它就被 @FunctionalInterface 注解。

三、注解的提取

前面我通过用标签来比作注解,前面的内容是讲怎么写注解,然后贴到哪个地方去,而现在我们要做的工作就是检阅这些标签内容。 形象的比喻就是你把这些注解标签在合适的时候取下来,然后检阅上面的内容信息。

要想正确检阅注解,离不开一个手段,那就是反射。

注解与反射

注解通过反射获取。可以通过 Class 类的 isAnnotationPresent() 方法判断是否应用了某个注解,使用isAnnotation()判断是否是注解

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}
public boolean isAnnotation(){}

然后可以使用 Class 类的getAnnotation() 方法或getAnnotations() 方法来获取 Annotation 对象。

public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
public Annotation[] getAnnotations() {}

前一个方法返回指定类型的注解,后一个方法返回注解到这个元素上的所有注解。

如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如

@Inherited
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String name() default "";

    int age() default 0;
}

-----------------------------------------------------------------------------
@MyAnnotation(name = "haungxin",age=20)
public class App {

    public static void main(String[] args) {
        //判断MyAnnotation注解是否在App这个类上出现
        if(App.class.isAnnotationPresent(MyAnnotation.class)){
            //获取App类上的注解
            MyAnnotation myAnnotation = App.class.getAnnotation(MyAnnotation.class);

            //获取注解的值
            System.out.println("name="+myAnnotation.name());
            System.out.println("age="+myAnnotation.age());
        }
    }
}

执行结果:

四、注解的使用场景

我相信讲到这里大家都很熟悉了注解,但是有不少同学肯定会问,注解到底有什么用呢?

对啊注解到底有什么用?

我们不妨将目光放到 Java 官方文档上来。

文章开始的时候,我用标签来类比注解。但标签比喻只是我的手段,而不是目的。为的是让大家在初次学习注解时能够不被那些抽象的新概念搞懵。既然现在,我们已经对注解有所了解,我们不妨再仔细阅读官方最严谨的文档。

注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。

注解有许多用处,主要如下:

  • 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
  • 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
  • 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取

值得注意的是,注解不是代码本身的一部分。注解主要针对的是编译器和其它工具软件(SoftWare tool)。

当开发者使用了Annotation 修饰了类、方法、Field 等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的代码来提取并处理 Annotation 信息。这些处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。 所以现在,我们可以给自己答案了,注解有什么用?给谁用?给编译器或者 APT 用的。

因此,注解能干什么,取决于你想用它来干什么!

总结

  1. 如果注解难于理解,你就把它类同于标签,标签为了解释事物,注解为了解释代码。
  2. 注解的基本语法,创建如同接口,但是多了个 @ 符号。
  3. 注解的元注解。
  4. 注解的属性。
  5. 注解主要给编译器及工具类型的软件用的。
  6. 注解的提取需要借助于 Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。

参考:

https://blog.csdn.net/briblue/article/details/73824058

留言区

还能输入500个字符