Kotlin 的注解

从这篇文章我们一起来看下 Kotlin 中的注解。Kotlin 中的注解是 100% 与 Java 注解兼容的,有很多相同的地方,但是也有一些不同的地方。一起来瞅瞅吧~

1. 注解的本质

注解实际上就是一种代码标签,它作用的对象是代码。它可以给特定的注解代码标注一些额外的信息。然而这些信息可以选择不同保留时期,比如源码期、编译期、运行期。然后在不同时期,可以通过某种方式获取标签的信息来处理实际的代码逻辑,这种方式常常就是我们所说的反射

2. 注解的定义

在 Kotlin 中注解核心概念和 Java 一样,注解就是为了给代码提供元数据。并且注解是不直接影响代码的执行。一个注解允许你把额外的元数据关联到一个声明上,然后元数据就可以被某种方式(比如运行时反射方式以及一些源代码工具)访问

3. 注解的声明

在 Kotlin 中的声明注解的方式和 Java 稍微不一样,在 Java 中主要是通过 @interface关键字来声明,而在Kotlin 中只需要通过 annotation class 来声明, 需要注意的是在 Kotlin 中编译器禁止为注解类指定类主体,因为在 Kotlin 中注解只是用来定义关联的声明和表达式的元数据的结构

  • Kotlin 注解声明
package com.mikyou.annotation
//和一般的声明很类似,只是在class前面加上了annotation修饰符
annotation class TestAnnotation(val value: String)
  • Java 注解声明
package com.mikyou.annotation;
//java中的注解通过@interface关键字进行定义,它和接口声明类似,只不过在前面多加@
public @interface TestAnnotation {
    String value();
}

4. 注解的应用

在上一步我们知道了如何声明和定义标签了,那么接下来就是用这个标签,如何把我们定义好的标签贴到指定的代码上。在 Kotlin 中使用注解和 Java 一样。要应用一个注解都是 @注解类名

@Target(AnnotationTarget.FUNCTION)
@Retention(value = AnnotationRetention.RUNTIME)
annotation class TestAnnotation(val value: Int)//和一般的声明很类似,只是在class前面加上了annotation修饰符

class Test {
    @TestAnnotation(value = 1000)
    fun test() {//给test函数贴上TestAnnotation标签(添加TestAnnotation注解)
        //...
    }
}

在很多常见的 Java 或 Kotlin 框架中大量使用了注解,比如我们最常见的 JUnit 单元测试框架:

class ExampleUnitTest {
    @Test //@Test注解就是为了告诉JUnit框架,这是一个测试方法,当做测试调用。
    fun addition_isCorrect() {
        assertEquals(4, 2 + 2)
    }
}

Kotlin 中注解类中还可以拥有注解类作为参数,不妨来看下 Kotlin 中对 @Deprecated这个注解源码定义,以及它的使用。@Deprecated 注解在原来的 Java 基础增强了一个 ReplaceWith 功能.。可以直接在使用了老的 API 时,编译器可以根据 ReplaceWith 中的新 API,自动替换成新的 API。这一点在 Java 中是做不到的,你只能点击进入这个 API 查看源码来正确使用新的 API。

//@Deprecated注解比Java多了ReplaceWith功能, 这样当你在调用remove方法,编译器会报错。使用代码提示会自动IntelliJ IDEA不仅会提示使用哪个函数提示替代它,而且会快速自动修正。
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"), level = DeprecationLevel.ERROR)//定义的级别是ERROR级别的,这样当你在调用remove方法,编译器会报错。
@kotlin.internal.InlineOnly
public inline fun <T> MutableList<T>.remove(index: Int): T = removeAt(index)

@Deprecated 注解的 remove 函数使用:

//Deprecated注解的使用
fun main(args: Array<String>) {
    val list = mutableListOf("a", "b", "c", "d", "e")
    list.remove(3)//这里会报错, 通过remove函数注解定义,这个remove函数在定义的level是ERROR级别的,所以编译器直接抛错
}

图片描述

图片描述

最后来看下 @Deprecated 注解的定义:

@Target(CLASS, FUNCTION, PROPERTY, ANNOTATION_CLASS, CONSTRUCTOR, PROPERTY_SETTER, PROPERTY_GETTER, TYPEALIAS)
@MustBeDocumented
public annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""),//注解类中构造器可以使用注解类作为函数参数
        val level: DeprecationLevel = DeprecationLevel.WARNING
)
@Target()
@Retention(BINARY)
@MustBeDocumented
public annotation class ReplaceWith(val expression: String, vararg val imports: String)

注意:注解类中只能拥有如下类型的参数: 基本数据类型、字符串、枚举、类引用类型、其他的注解类(例如Deprecated注解类中的ReplaceWith注解类)

5. Kotlin 中的元注解

和 Java 一样在 Kotlin 中,一个 Kotlin 注解类自己本身也可以被注解,可以给注解类加注解。我们把这种注解称为元注解,可以把它理解为一种基本的注解,也可以把它理解为一种特殊的标签,用于标注标签的标签。

Kotlin 中的元注解类定义于 kotlin.annotation 包中,主要有: @Target@Retention@Repeatable@MustBeDocumented 4 种元注解, 相比 Java 中 5 种元注解: @Target@Retention@Repeatable@Documented、**@Inherited **少了 @Inherited元注解。

5.1 @Target 元注解

介绍

Target 顾名思义就是目标对象,也就是这个标签作用于哪些代码中目标对象,可以同时指定多个作用的目标对象。

源码定义

@Target(AnnotationTarget.ANNOTATION_CLASS)//可以给标签自己贴标签
@MustBeDocumented
//注解类构造器参数是个vararg不定参数修饰符,所以可以同时指定多个作用的目标对象
public annotation class Target(vararg val allowedTargets: AnnotationTarget)

@Target 元注解作用的目标对象

在@Target注解中可以同时指定一个或多个目标对象,那么到底有哪些目标对象呢?这就引出另外一个AnnotationTarget 枚举类:

public enum class AnnotationTarget {
    CLASS, //表示作用对象有类、接口、object对象表达式、注解类
    ANNOTATION_CLASS,//表示作用对象只有注解类
    TYPE_PARAMETER,//表示作用对象是泛型类型参数(暂时还不支持)
    PROPERTY,//表示作用对象是属性
    FIELD,//表示作用对象是字段,包括属性的幕后字段
    LOCAL_VARIABLE,//表示作用对象是局部变量
    VALUE_PARAMETER,//表示作用对象是函数或构造函数的参数
    CONSTRUCTOR,//表示作用对象是构造函数,主构造函数或次构造函数
    FUNCTION,//表示作用对象是函数,不包括构造函数
    PROPERTY_GETTER,//表示作用对象是属性的getter函数
    PROPERTY_SETTER,//表示作用对象是属性的setter函数
    TYPE,//表示作用对象是一个类型,比如类、接口、枚举
    EXPRESSION,//表示作用对象是一个表达式
    FILE,//表示作用对象是一个File
    @SinceKotlin("1.1")
    TYPEALIAS//表示作用对象是一个类型别名
}

5.2 @Retention 元注解

介绍

Retention 对应的英文意思是保留期,当它应用于一个注解上表示该注解保留存活时间,不管是Java还是Kotlin 一般都有三种时期: 源代码时期(SOURCE)编译时期(BINARY)运行时期(RUNTIME)

源码定义

@Target(AnnotationTarget.ANNOTATION_CLASS)//目标对象是注解类
public annotation class Retention(val value: AnnotationRetention = AnnotationRetention.RUNTIME)//接收一个参数,该参数有个默认值,默认是保留在运行时期

@Retention 元注解的取值

@Retention 元注解取值主要来源于 AnnotationRetention 枚举类

public enum class AnnotationRetention {
    SOURCE,//源代码时期(SOURCE): 注解不会存储在输出class字节码中
    BINARY,//编译时期(BINARY): 注解会存储出class字节码中,但是对反射不可见
    RUNTIME//运行时期(RUNTIME): 注解会存储出class字节码中,也会对反射可见, 默认是RUNTIME
}

5.3 @MustBeDocumented元注解

介绍

该注解比较简单主要是为了标注一个注解类作为公共API的一部分,并且可以保证该注解在生成的API文档中存在。

源码定义

@Target(AnnotationTarget.ANNOTATION_CLASS)//目标对象只能是注解类
public annotation class MustBeDocumented

5.4 @Repeatable元注解

介绍

这个注解决定标注的注解在一个注解在一个代码元素上可以应用两次或两次以上。

源码定义

@Target(AnnotationTarget.ANNOTATION_CLASS)//目标对象只能是注解类
public annotation class Repeatable

5.5 为啥Kotlin去掉了Java中的@Inherited元注解

Java 中的 @Inherited 元注解介绍

Inheried 顾名思义就是继承的意思,但是这里需要注意并不是表示注解类可以继承,而是如果一个父类被贴上 @Inherited 元注解标签,那么它的子类没有任何注解标签的话,这个子类就会继承来自父类的注解。类似下面的例子:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}

@TestAnnotation
class Animal {
    //...
}

class Cat extends Animal{//也会拥有来自父类Animal的@TestAnnotation注解
    //...
}

Kotlin 为啥不需要 @Inherited 元注解?

关于这个问题实际上在 Kotlin 官网的 discuss 中就有人提出了这个问题,具体感兴趣的可以去看看:Inherited annotations and other reflections enchancements

这里大概说下原因,我们都知道在 Java 中,无法找到子类方法是否重写了父类的方法。因此不能继承父类方法的注解。然而 Kotlin 目前不需要支持这个 @Inherited 元注解,因为 Kotlin 可以做到,如果反射提供了override 标记而且很容易做到。

6. 注解的使用场景

  • 提供信息给编译器: 编译器可以利用注解来处理一些,比如一些警告信息,错误等;
  • 编译阶段时处理: 利用注解信息来生成一些代码,在Kotlin生成代码非常常见,一些内置的注解为了与Java API的互操作性,往往借助注解在编译阶段生成一些额外的代码;
  • 运行时处理: 某些注解可以在程序运行时,通过反射机制获取注解信息来处理一些程序逻辑。