Kotlin/Java中的Annotation(注解)详解

Java基础

浏览数:245

2019-8-22

从JDK1.5开始,Java引入了一种新的注释机制-Annotation,中文名称一般叫注解,它一般作为说明信息,与程序的业务逻辑无关。

既然注解仅仅是一种说明信息,为什么我们还要了解它呢?因为它还广泛地应用于一些工具或框架中。从官方的定义来看,Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法,当然我们都知道官方的定义往往都是看不懂的,因此我们需要尽量用通俗一点的东西去理解它。鉴于大家对Java本身的Annotation都不怎么熟悉,因此本文先介绍Java中的注解,再介绍Kotlin的版本。

Java Annotation组成部分

在源码(java.lang.annotation)中,有几个比较重要的类:
(1)Annotation.java

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

从代码来看并无特别之处,唯一值得注意的是 annotationType()这个方法的返回值,这里暂时不讲
(2)ElementType.java

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

ElementType是一个枚举类,它用来指定Annotation的类型,表明Annotation可以用在什么地方
例如,TYPE表示这个Annotation可以用在类/接口/Annotation或者枚举上,CONSTRUCTOR表示它可以用于构造器上,METHOD表示可以用来修饰方法
需要注意的一点是,一个Annotation可以与多个ElementType关联
TYPE_PARAMETER/TYPE_USE是JDK1.8的新特性,它代表此Annotation可以用于泛型
(3)RetentionPolicy.java

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

RetentionPolicy正如它名字所示,它代表了Annotation“保留”的策略
SOURCE代表这个注解在编译器处理完之后就被抛弃(好狠),这意味着它的信息信息仅存在于编译器处理期间。
CLASS代表编译器将注解存储于类对应的.class文件中,但是在运行时不能通过JVM读取,在Java中这是Annotation的默认行为(在Kotlin中默认行为是RUNTIME)
RUNTIME则代表编译器将注解存储于.class文件中,并且可由反射获取,由于Kotlin的语言特性,它在是Kotlin中的默认策略。
很显然,一个Annotation只能和一个RetentionPolicy关联
总结:一个完整的Annotation与多个ElementType和一个RetentionPolicy相关联

在Java中定义Annotation

理解了以上的三个类,掌握定义Annotation的方式就很容易了:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface MyAnnotation {
}

以上代码定义了一个名字叫MyAnnotation的注解,它可以用来修饰类/接口/Annotation或者枚举,并且将注解存储于类对应的.class文件中。现在,我们可以在代码里通过@MyAnnotation来使用它了:

@MyAnnotation
class water{
    @MyAnnotation //My Annotation不能作用于方法
    public void flow()
    {
        
    }
    
}

在定义注解时需要注意以下几点:
(1)定义注解时,必须通过@interface方式,而不是通过implement Annotation的方式,因为Annotation接口的实现细节都是由编译器完成,而不是通过我们的代码去完成的。通过@interface定义注解后,该注解不能继承其他的注解或接口。
(2)@Target(ElementType.TYPE) 的意思就是指定该注解的类型是ElementType.TYPE,当然,你可以定义多个ElementType:

@Target({ElementType.ANNOTATION_TYPE,ElementType.METHOD})
@interface YourAnnotation{
    
}

当然,不指定也是行的,这时候此注解可以用在任何地方
(3)@Retention(RetentionPolicy.CLASS)表示这个注解的保留策略为CLASS(将注解存储于.class文件中,但是不能被JVM访问),在Java中如果不指定的话默认保留策略为RetentionPolicy.CLASS
因此,在定义一个注解需要三个步骤- (1)实现接口 (2)定义类型 (3)定义保留策略

Java中的常见注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

@Documented标记表示被注解的内容会被收录入javadoc中,只能用在其他的注解上。喜感的是,它在自己的定义中使用了它自己。

@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Deprecated {
}

@Deprecated 表示所标注的内容不再被建议使用,它可以用在任何地方

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

@Inherited 表示它所标注的Annotation将具有继承性,这意味着它所标注的类的子类也将拥有这个标注(子类默认是不继承注解的)。

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {

    String[] value();

}

@SuppressWarnings可以让编译器忽略掉对它所标注的内容的警告

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    /**
     * Indicates the <em>containing annotation type</em> for the
     * repeatable annotation type.
     * @return the containing annotation type
     */
    Class<? extends Annotation> value();
}

重复注释允许相同注释在声明使用的时候重复使用超过一次,这是Java 8中的新特性,不过目前来看并没有什么卵用。

让Java程序认识注解

如果注解仅仅是用于给程序员描述信息的话,那实际上注解是没有什么实质性作用的。为了编写更为灵活和智能的程序,我们需要在程序中识别注解。

public interface AnnotatedElement {
    default boolean isAnnotationPresent(Class<? extends Annotation> var1) {
        return this.getAnnotation(var1) != null;
    }

    <T extends Annotation> T getAnnotation(Class<T> var1);

    Annotation[] getAnnotations();

    default <T extends Annotation> T[] getAnnotationsByType(Class<T> var1) {
        Annotation[] var2 = this.getDeclaredAnnotationsByType(var1);
        if(var2.length == 0 && this instanceof Class && AnnotationType.getInstance(var1).isInherited()) {
            Class var3 = ((Class)this).getSuperclass();
            if(var3 != null) {
                var2 = var3.getAnnotationsByType(var1);
            }
        }

        return var2;
    }

    default <T extends Annotation> T getDeclaredAnnotation(Class<T> var1) {
        Objects.requireNonNull(var1);
        Annotation[] var2 = this.getDeclaredAnnotations();
        int var3 = var2.length;

        for(int var4 = 0; var4 < var3; ++var4) {
            Annotation var5 = var2[var4];
            if(var1.equals(var5.annotationType())) {
                return (Annotation)var1.cast(var5);
            }
        }

        return null;
    }

    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> var1) {
        Objects.requireNonNull(var1);
        return AnnotationSupport.getDirectlyAndIndirectlyPresent((Map)Arrays.stream(this.getDeclaredAnnotations()).collect(Collectors.toMap(Annotation::annotationType, Function.identity(), (var0, var1) -> {
            return var0;
        }, LinkedHashMap::<init>)), var1);
    }

    Annotation[] getDeclaredAnnotations();
}

AnnotatedElement接口定义了一些判断注解以及获取注解的方法。isAnnotationPresent方法可以判断该元素是否拥有指定注解类的注解,getAnnotation(Class<T> var1) 则返回指定注解类的注解,getAnnotations()则会返回所有的注解。
实现了这个接口的类有: Class,Constructor,Executable,Field,Method,Package,Parameter,AccessibleObject
这些类都位于java.lang.reflect包,这意味着我们的程序需要通过反射来识别注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation {
}

@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation2{
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
@interface MyAnnotation3{
}

@MyAnnotation
class Country{
    @MyAnnotation
    int pop = 0;
    int coin = 0;
    String name = "";
    public Country(String name,int pop,int coin){
        this.name = name;
        this.pop = pop;
        this.coin = coin;
    }
    @MyAnnotation2
    public String describe()
    {
        return name+" pop: "+pop+" coin: "+coin;
    }
    @MyAnnotation2
    public int doublepop()
    {
        return pop*2;
    }
    @MyAnnotation3
    public int averageCoin()
    {
        if(pop == 0) return Integer.MAX_VALUE;
        else return coin/pop;
    }
}

public class Anotest {

    public static void main(String[] args) {
        Country sr = new Country("Ravenland",200,10000);
        Class<Country> c = Country.class;
        if(c.isAnnotationPresent(MyAnnotation.class))
        {
            System.out.println("Class Country has MyAnnotation");
        }
        Method[] methods = c.getMethods();
        for(Method method : methods)
        {
            Annotation[] aList = method.getAnnotations();
            if(aList!=null && aList.length > 0)
            {
                for(Annotation b : aList)
                {
                    System.out.println(method.getName()+"Has Annotation: "+b);
                }
            }
        }
    }
}

输出结果:

Class Country has MyAnnotation
doublepopHas Annotation: @test.MyAnnotation2()
describeHas Annotation: @test.MyAnnotation2()

以上的例子深刻地表明只有RetentionPolicy.RUNTIME的注解能够通过反射访问。
注解广泛地使用在各种工具(例如Retrofit,GSON等)和框架(JUnit,DataBinding)中。
如果你对反射还不太熟悉也没太大问题,不久之后我也会介绍反射的一些概念。

在Kotlin中使用注解

同Java一样,Kotlin中也提供了对注解的支持,大部分相关的类位于包kotlin.annotation中。
(1)AnnotationTarget.kt

public enum class AnnotationTarget {
    /** Class, interface or object, annotation class is also included */
    CLASS,
    /** Annotation class only */
    ANNOTATION_CLASS,
    /** Generic type parameter (unsupported yet) */
    TYPE_PARAMETER,
    /** Property */
    PROPERTY,
    /** Field, including property's backing field */
    FIELD,
    /** Local variable */
    LOCAL_VARIABLE,
    /** Value parameter of a function or a constructor */
    VALUE_PARAMETER,
    /** Constructor only (primary or secondary) */
    CONSTRUCTOR,
    /** Function (constructors are not included) */
    FUNCTION,
    /** Property getter only */
    PROPERTY_GETTER,
    /** Property setter only */
    PROPERTY_SETTER,
    /** Type usage */
    TYPE,
    /** Any expression */
    EXPRESSION,
    /** File */
    FILE,
    /** Type alias */
    @SinceKotlin("1.1")
    TYPEALIAS
}

在Kotlin中,AnnotationTarget是一个枚举类,它的作用和ElementType类似,表示注解能应用到哪些地方。
其中需要特别注意的是FIELD和PROPERTY,Kotlin的类不能有字段(FIELD)而只有属性(PROPERTY),只有在使用自定义访问器时才可能需要有一个后备字段。另外,由于在Kotlin中的单个申明往往对应了多个Java声明(例如var属性),因此在标注中也特意进行了PROPERTY_GETTER和PROPERTY_SETTER来特意区分它们。
在Kotlin中也可以对文件(FILE)和别名(TYPEALIAS)进行注解。
如果在声明注解时没有特意指明AnnotationTarget,说明这个注解可以用到任何地方(除了泛型、表达式和文件)
(2)AnnotationRetention.kt

public enum class AnnotationRetention {
    /** Annotation isn't stored in binary output */
    SOURCE,
    /** Annotation is stored in binary output, but invisible for reflection */
    BINARY,
    /** Annotation is stored in binary output and visible for reflection (default retention) */
    RUNTIME
}

AnnotationRetention和Java中的RetentionPolicy相对应,表示注解的保留策略。在Kotlin中的BINARY和Java中的CLASS对应,都代表无法通过反射访问,但是会被保留在class文件中。
在Kotlin中,默认的保留策略为RUNTIME,这表明我们可以通过反射在程序中获取它。
(3)在Kotlin中定义注解
在Kotlin中声明注解和声明普通的类很相似,区别在于在class前面需要加上annotation关键字:

/*不接收参数的注解*/
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.BINARY)
annotation class MyAnnotation

/*接受参数的注解*/
annotation class MyAnnotation2(val name : String)

/*接受多个参数的注解*/
annotation class MyAnnotation3(vararg val name : String)

Kotlin中的注解目标

由于在Kotlin中的单个申明往往对应了多个Java声明,例如,一个Property对应了一个Field和Getter和Setter,为了使标注更为精确,Kotlin中还允许使用点目标。
点目标的语法为 @目标:注解名 例如:@get:MyAnnotation

class Country{
    /*对属性的getter使用注解*/
    @get:MyAnnotation2("Editable")
    var name : String = ""

    /*对属性使用注解*/
    @MyAnnotation2("Editable")
    var pop : Int = 0

    /*对属性的Setter使用注解*/
    @MyAnnotation3("Editable","Can be below 0")
    @set:MyAnnotation3
    var coin : Int = 0

    /*对生成的Field使用注解*/
    /*具有setter的属性一般会自动生成backing field(后备字段)*/
    @field:MyAnnotation2("")
    var army : Int = 1000
    
}

在Kotlin中支持以下点目标:
property:代表kotlin中的属性,不能被Java的注解所应用
field:为属性生成的字段(包括后备字段)
get:属性的getter
set:属性的setter
receiver:扩展函数/属性的接收者
param:构造函数的参数
setparam:属性setter的参数
delegate:委托属性存储委托实例的字段
file:在文件中声明的顶层函数与类
如果你希望你的注解使用类作为类型参数的话,你可以定义为:
annotation class MyClassAnnotation(val clazz : KClass<out Any>)
注意必须out Any,否则泛型参数无法协变导致你只能使用Any类作为参数

    @MyClassAnnotation(NewsAdapter::class)
    fun nothingleft()
    {
        
    }

KClass是Kotlin中与Java中Class类对应的类,用于Kotlin中的反射。
如果要使用泛型类作为注解参数,则需要通过星形投影

    annotation class ListAnnotation(val clazz : KClass<out List<*>>)
    
    /*把List作为注解参数*/
    @ListAnnotation(List::class)
    fun sum()
    {
        
    }

星形投影表示你不知道关于泛型实参的任何信息,比如在上面的例子中,你只知道List本身,而不知道它具体的泛型实参
由此可见,在Kotlin中应用注解的语法和Java很相似,但Kotlin中你注解的目标要比Java更广。
当然,还是那句话,注解给人看是没多大意义的,还是需要在程序里处理,这就需要掌握反射,不过,在介绍反射之前,我们需要对Java虚拟机中的内存模型进行一点简单的了解……
反射参考:http://www.jianshu.com/p/5adf5f1c49e8

作者:黑心石