kotlin入门潜修之特性及其原理篇—注解

本文收录于 kotlin入门潜修专题系列,欢迎学习交流。

创作不易,如有转载,还请备注。

注解

注解在编程语言中是一种特殊的机制、是用来描述代码的元数据。在我看来,注解首先简洁了代码,使得我们编码变得言简意赅;其次,注解解耦了系统,同时还让系统之间保持了一定的联系(比如不再像配置文件那样,和代码完全独立);最后,注解能及时有效的反馈错误(编译时报错)。

看到这里可能还是不太明白什么是注解,如果使用通俗的白话来表示一下注解是什么意思呢?

我们可以认为,注解就是一种注释,只不过这种注释是给两个角色看的:一个是我们,能够从注解中看出代码的意义来;另一个则是注解处理器(编译器),这个就是注解实际起作用的解释引擎,它会对注解信息做出解释,并执行相应的动作。比如基于注解的编译警告、依赖注入等等。注解处理引擎也是注解机制中最主要的存在。

如果还不明白,那就接着往下看。

java中的注解

java语言提供了很多内置的注解,比如我们常见的下面两种注解:

public class Test extends Thread {
    @Override
    public void run() {
        super.run();
    }
    @Deprecated
    public void m1() {
    }
}

没错,我们复写的run方法上面的 @Override就是个注解,表示该方法是超类中的方法,我们在子类进行了复写。 @Deprecated注解则表示该方法不建议被使用了,以后可能会被废弃。

从代码可以看出来,注解的使用语法是:@+注解名。此外注解还可以接受参数,比如我们常见的SuppressWarnings注解(作用是抑制警告),示例如下:

public class Test extends Thread {
    @Deprecated
    public void m1() {
    }

    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        Test t = new Test();
        t.m1();
    }
}

正常来讲,因为m1方法使用了 @Deprecated
注解修饰,所以我们在main方法调用m1的时候,编译器会警告我们m1方法已经不建议使用,此时,如果我们在m1方法上添加 @SuppressWarnings("deprecation")这个注解,就可以禁止掉这个警告提示。

当然,这个只是SuppressWarnings注解完成的一个功能,实际上它还可以完成其他许多的功能,这里不再阐述,只需要关注注解是可以接收参数这个特性即可。

自定义java注解

前面提到的都是java自带的注解,如果有需要,我们也可以自定义注解,如果不熟悉自定义注解,则可以参考系统提供的注解写法,比如我们先来看下上个章节中的SuppressWarnings这个注解的定义,示例如下:

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

由上面代码可知,SuppressWarnings注解包含了一个参数,该参数类型是字符串数组,这就是注解入参的写法:我们只需要像定义方法一样定义需要的入参及其类型即可。此外,我们发现,在使用SuppressWarnings的时候,我们并没有指定参数的名称,而是直接传入了“deprecation”这个值,这是为什么?如果有多个参数的时候还能这么传入吗?

这是java中的一个默认语法,对于只有一个入参的注解,我们可以使用value进行命名,使用value进行命名的参数,在传参的时候可以省略其名称。比如上面的SuppressWarnings("deprecation")就是省略了value的缘故,其全称可以写为:SuppressWarnings(value="deprecation")。

注意,只能当注解只需要一个参数,且该参数被命名为value的时候,才能省略。

最后,我们发现在SuppressWarnings注解上面还有一些注解,比如Target注解、Retention注解,这些注解被称为元注解,只能用于修饰注解类型。

Target注解和Retention注解是我们在定义注解的时候常用的两个元注解。使用Target修饰的注解,用来表明我们自定义注解可以修饰的目标,比如可以用来修饰方法、成员变量、包等等,具体的修饰目标如下所示:

/** 可以用于修饰类, 接口 (包括注解类型), 或者枚举类 */
    TYPE,
    /** 用于修饰字段 (包括枚举常量) */
    FIELD,
    /** 用于修饰方法 */
    METHOD,
    /** 用于修饰形参 */
    PARAMETER,
    /** 用于修饰构造方法*/
    CONSTRUCTOR,
    /** 用于修饰本地变量 */
    LOCAL_VARIABLE,
    /**  用于修饰注解*/
    ANNOTATION_TYPE,
    /** 用于修饰包 */
    PACKAGE,
    /**
     * 用于修饰类型参数,从jdk1.8开始支持
     */
    TYPE_PARAMETER,
    /**
     * 可用于修饰各种类型
     * @since 1.8
     */
    TYPE_USE

而Retention注解则表示我们自定义注解的生效时机,它总共支持三个值,如下所示:

    /**
     * 只在源代码中生效,编译的时候会被擦除
     */
    SOURCE,

    /**
     * 注解信息会被编译器编译到class文件中,但无法再运行时保持,这意味着我们无法再运行时采用反射来获取到这个注解,这个是所有注解的默认行为
     */
    CLASS,

    /**
     * 注解信息会被编译到class文件中,而且在运行时保持了该注解信息,可以使用反射机制获取到该注解信息
     */
    RUNTIME

结合上面的信息,我们演示下自定义注解的写法,如下所示:

public @interface MyAnnotation {
    String firstParam();
    int secondParam();
}

上面代码我们定义了一个接收两个入参的注解,其语法为权限修饰符 + @interface + 注解名。该注解信息同时会在编译期和运行期生效(参见上面阐述过的默认取值)。

乍看起来,注解的写法有点像接口的写法,形式上确实是这样,但需要注意以下几点:

  1. 同接口一样,注解只能使用包权限修饰符或者public权限修饰符进行修饰。
  2. 和接口不同的是,注解的关键字是@interface,并不是interface

那么,如何使用我们自定义的注解呢?这个很简单,其使用姿势与使用系统提供的注解姿势一致,如下所示:

@Retention(RetentionPolicy.RUNTIME)
public @MyAnnotation(firstParam = "param1", secondParam = 1)
public class Test {}

使用注解的姿势就是@+注解名,然后附带上注解需要的参数即可。

kotlin中的注解

了解完java中的注解后,再来看下kotlin中的注解。

首先,在kotlin中,同样为我们提供了几个常见的内置注解,这些注解位于kotlin.annotation包下,它们只能用于修饰注解类型,也就是元注解。介绍如下:

//注解的修饰目标,与java类似,但是有很多不同
@Target
//同java基本一致,定义有稍微不同
@Retention
//新注解,表示一个注解可以在同一个地方写多遍,
//在1.8版本之前的jvm,只支持Retention为Source的写法
@Repeatable
//用该注解修饰的注解,将会被视为api方法的一部分,
//在生成文档的时候将会保留注解相关信息
@MustBeDocumented

下面简单介绍下上面的几个元注解。

首先,来看下Target修饰的注解可以用于修饰哪些目标,如下所示:

/**用于修饰类,、接口、对象类(object)、注解类*/
    CLASS,
    /** 只能用于修饰注解类,实际上Target注解本身就是使用该类型修饰的,所以Target只能用于注解 */
    ANNOTATION_CLASS,
    /** 用于修饰泛型类型参数 (直接写本篇文章时,暂未支持) */
    TYPE_PARAMETER,
    /** 用于修饰属性*/
    PROPERTY,
    /** 用于修饰字段, 包括属性的后备字段 */
    FIELD,
    /** 用于修饰本地变量 */
    LOCAL_VARIABLE,
    /** 方法或者构造方法中的值参数 */
    VALUE_PARAMETER,
    /** 只能用于修饰构造方法(第一或者第二)*/
    CONSTRUCTOR,
    /** 用于修饰方法,不包括上面的构造方法 */
    FUNCTION,
    /**只能用于修饰getter属性 */
    PROPERTY_GETTER,
    /**只能用于修饰setter属性 */
    PROPERTY_SETTER,
    /**同java,用于修饰类型 */
    TYPE,
    /**用于修饰任意的表达式*/
    EXPRESSION,
    /** 用于修饰文件*/
    FILE,
    /** 用于修饰别名类型,从kotlin1.1开始支持 */
    TYPEALIAS

然后来看下Retention修饰的注解可以修饰哪些目标,如下所示:

    /** 源代码层次 */
    SOURCE,
    /** 存在于编译期,运行时不可见 */
    BINARY,
    /** 即存在于编译期,又能在运行时访问到,默认即是该值*/
    RUNTIME

而对于Repeatable和MustBeDocumented注解都没有参数,因此只有单一的意义,此处略过。

那么kotlin中的注解写法和java中的注解有什么不同吗?这里通过代码展示下kotlin中注解的使用方式,注意代码中的注释:

//这里定义了一个注解,可被文档化,能修饰类、属性、
//构造方法、本地变量,除此之外不能修饰其他目标
//同时该注解的信息既在编译器保留,同时在运行时也能获取到
@MustBeDocumented
@Target(AnnotationTarget.CLASS, AnnotationTarget.CONSTRUCTOR, AnnotationTarget.PROPERTY, AnnotationTarget.LOCAL_VARIABLE)
@Retention(AnnotationRetention.RUNTIME)
annotation class TestAnnotation(val value: String)

@TestAnnotation("class")//正确,可以修饰类
class Test {
    @TestAnnotation("property"))//正确,可以修饰属性
    private val t1 = "test"

    @TestAnnotation("constructor"))//正确,可以修饰构造方法
    constructor()

    @TestAnnotation("method"))//!!!错误,不能修饰方法
    fun m1() {
        @TestAnnotation("local_variable"))//正确,可以修饰本地变量
        val i = 1;
    }
}

可见kotlin中的注解与java中的注解大同小异,但需要注意的是kotlin自定义注解的语法,不再是像java一样使用@interface来进行定义,而是使用annotation class这种语法。

kotlin精确注解

“精确注解”产生的原因,主要是kotlin注解与java层面上注解的不同而引起的,先来看个例子:

class Test( val param1: Int)

上面是一个极其简单的kotlin类声明,主构造方法接收一个Int类型的参数param1,那么这段代码生成的字节码是什么呢?来看下:

public final class Test {
  // access flags 0x12
  private final I param1
  // access flags 0x11
  public final getParam1()I
   //...省略一些字节码
  // access flags 0x1
  public <init>(I)V
   //...省略一些字节码

由字节码可知,对于上面的代码,kotlin为我们生成了构造方法,并为param1生成了一个属性成员以及一个公有的get方法,那么问题来了,如果我们在param1上加上注解,该注解到底会在哪儿生效呢?比如,下面一段代码:

//我们定义了一个注解TestAnnotation
annotation class TestAnnotation
//使用注解TestAnnotation修饰param1,此时注解作用的目标是什么?
class Test(@TestAnnotation val param1: Int)

上面代码中,注解TestAnnotation到底作用于param1对应的属性成员还是其对应的get方法呢?很简单,来看下字节码,如下所示:

//...省略部分字节码
  public <init>(I)V
    @LTestAnnotation;()
//...省略部分字节码

通过查看字节码,我们发现,TestAnnotation被编译到了构造方法中,即默认修饰的是构造方法中的参数,而不是属性和get方法!那么如何明确指定TestAnnotation注解在java层面上的修饰目标呢?这就引入了“精确注解”的概念,所谓“精确注解”是指,kotlin允许我们精确指定注解在java层面上的修饰目标。

比如,同样是上面的代码,我们想让注解实际作用域java层面上的get方法,我们就可以这么写:

class Test(@get:TestAnnotation val param1: Int)

其对应的字节码如下所示:

//...省略部分字节码
  public final getParam1()I
  @LTestAnnotation;()
//...省略部分字节码

通过上面字节码我们发现,实际上TestAnnotation注解已经最用于param1对应的get方法上了!这就是“精确注解”。

kotlin为我们提供了多个“精确注解”,当我们使用这些注解的时候,其相应的作用罗列如下:
—file:作用于文件
—property :用该值修饰的注解对java来说是不可见的
—field: 字段
—get : 用于修饰属性的getter
—set :用于修饰属性的setter
—receiver :用于修饰扩展属性以及方法的receiver
—param :用于修饰构造参数
—setparam :用于修饰属性的setter参数
—delegate :用于修饰委托属性
如果没有指定“精确注解”,那么kotlin编译器会根据注解上的target取值来决定修饰哪个目标,如果有多个目标,kotlin规定了他们的优先级:第一优先级是param;然后是property;最后是field。

注解背后的机制

本小节来结合Retention看下kotlin注解背后的原理,首先看下要分析的代码,如下所示:

@Target(AnnotationTarget.CLASS)
annotation class TestAnnotation(val value: String)
//修饰类Test
@TestAnnotation("class")
class Test {
}

其生成的字节码如下所示:

public abstract @interface TestAnnotation implements java/lang/annotation/Annotation  {
  @Lkotlin/annotation/Target;(allowedTargets={Lkotlin/annotation/AnnotationTarget;.CLASS})

  @Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)

  @Ljava/lang/annotation/Target;(value={Ljava/lang/annotation/ElementType;.TYPE})

  // access flags 0x401
  public abstract value()Ljava/lang/String;
}

由上面字节码可以总结如下:

  1. kotlin注解实际上会被编译成实现java.lang.annotation.Annotation接口的一个抽象类。这个与java相一致,因为在java中只有实现了Annotation接口的才被当做注解对待(而且还不能是自己实现,必须是系统实现)!
  2. 注解的入参,实际上会被编译成抽象的公有方法。这些公有的抽象方法,就是用于获取注解参数的对外方法!比如我们想获取上述TestAnnotation注解的vlaue值,如下所示:
    Test::class.annotations.forEach {
        val anotation:TestAnnotation = it as TestAnnotation
        println(anotation.value)//打印 'class'
    }

其对应的字节码摘录如下所示:

   L8
    LINENUMBER 24 L8
    ALOAD 5
    INVOKEINTERFACE TestAnnotation.value ()Ljava/lang/String;//调用了value方法!!!
    ASTORE 6
   L9
  1. 如果我们不加任何元注解信息,则编译器会为我们加上java层面上的默认元注解(如默认的@Retention(RetentionPolicy.RUNTIME)
    ),但当我们显示使用kotlin语言指定Retention策略的时候,编译器则会同时提供kotlin层面的注解,如下所示:
//自定义注解信息
@Retention(AnnotationRetention.RUNTIME)
annotation class TestAnnotation(val value: String)
//生成的字节码
public abstract @interface TestAnnotation implements java/lang/annotation/Annotation  {
//kotlin层面的注解信息
  @Lkotlin/annotation/Retention;(value=Lkotlin/annotation/AnnotationRetention;.RUNTIME)
//java层面的注解信息
  @Ljava/lang/annotation/Retention;(value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME)
  // access flags 0x401
  public abstract value()Ljava/lang/String;
}
  1. 关于Retention注解,可以通过字节码来查看其取不同取值时的区别,比如我们定义如下代码:
@Retention(AnnotationRetention.CLASS)
annotation class TestAnnotation(val value: String)
//使用注解
@TestAnnotation("class")
class Test { }
//对应的字节码将会包含该注解信息,如下所示:
public final class Test {
  @LTestAnnotation;(value="class")
}

下面我们改变Retention的策略,如下所示:

//将Retention的策略改成SOURCE
@Retention(AnnotationRetention.SOURCE)
annotation class TestAnnotation(val value: String)

那么,此时我们将不能在字节码中看到注解信息!关于RUNTIME机制这里无法从字节码中进一步验证,只能通过代码来演示在运行时获取注解信息,这个会在下节案例实战中进行阐述。

注解案例实战

前面巴拉巴拉一大堆注解的语法,看来看去怎么感觉注解没有啥用?如果单纯像上面那样自己定义注解,然后拿着自定义的注解去修饰下目标,确实没有什么用!那么注解存在的意义是什么?除了几个系统自带的注解外,我们自定义的注解又该如何工作?

这些问题正是我们本小节要阐述的问题,现在我们来写一个demo,演示下实战中注解的应用。

这个demo的目的就是完成依赖注入功能!没错,就是很多框架提供的功能,这里我们提供一个非常“简版”的kotlin实现。

首先,先来了解下什么是依赖注入。在写代码的时候,如果在类A中使用到类B的对象的时候,我们一般会直接在类A中进行new B()操作,这样就满足了我们的需求。这么做有什么弊端呢?

这种写法的弊端就是,A类需要对B对象的创建以及其生命周期负责,这样显然会带来系统的高耦合性。举个简单的例子,试想一下,如果此时B的构造方法变了,不再是无参的构造方法,那么是不是也需要去变更A类中的代码?如果有多个类依赖了B,是不是要逐个修改这些类中的代码?答案是显然的,这种牵一发而动全身的系统绝对不是我们想要的,因此有一种解耦的设计模式(思想)就出现了,它就是控制反转。

控制反转要解决的问题就是,不再将B的创建放到A类中,而是交给A类的使用者,比如我们可以为A类提供一个构造方法,该构造方法接收B类型的对象,这样我们就可以在A中使用由外界传入的B对象。再进一步考虑,为了减免外部构造的复杂性(即使用的时候,我们无需关注B对象的构建,也无需关注A类是不是需要B对象),我们可以提供一个“中间者”来完成B对象的构造、赋值功能,这样就大大解耦了系统,这个“中间者”就是我们demo要实现的功能,也是各大框架中所谓的“容器”。

控制反转已经理解了,那么上面提到的依赖注入是什么?其实上面的描述的具体操作就是依赖注入,控制反转是一种设计模式、一种思想,而依赖注入是这种模式的一种具体实现。依赖注入使得获取依赖对象的方式反转了。

白话一大堆,说的自己都觉得玄乎其玄,其实最好的例子就是看代码!想起一句话,代码面前了无秘密!

demo的场景是这样的,我们有一个画笔类Paint,以及一个颜色类Color,我们将用这两个类完成绘制功能。很显然,按照常规方案,我们只需要在画笔内部生成一个Color对象,然后完成绘制即可,示意如下:

//color类,用于获取画笔的颜色
class Color {
    fun getColor(): String {
        return "red"
    }
}
//画笔
class Paint {
//这里我们直接在画笔类中生成了Color对象
    private val color = Color();
//完成绘制
    fun draw() {
        println("draw with color: " +color.getColor())
    }
}
//测试代码
fun main(args: Array<String>) {
    val paint = Paint()
    paint.draw()//打印 draw with color: red
}

上面就是我们常规的实现方案,但是根据上文分析可知,这种实现方式显然具有很强的耦合性,因此我们需要采用“控制反转”的思想来完成对Paint类中的color字段的赋值。

首先,我们定义一个注解,该注解位于test包下,如下所示:

package test
annotation class Inject //注解Inject,位于test包中

然后,我们抽象出来一个“中间者”,这个“中间者”的作用就是解析Inject注解、完成控制反转,如下所示:

//中间者Ioc类
class Ioc {
//定义了一个伴随对象,方便外部调用
    companion object {
//inject方法接收两个参数,一个是宿主对象,一个是宿主对象的类类型
//这个宿主对象是相对于注入对象来讲的,比如在本例中,Paint类型的对象
//就是宿主对象,而Color类型对象就是被注入的对象
        fun inject(obj: Any, clazz: KClass<out Any>) {
            for (memberProperty in clazz.memberProperties) {//遍历成员变量
                memberProperty.annotations.forEach {//读取成员变量上面的注解
                    if (it.annotationClass.qualifiedName.equals("test.Inject")) {//如果该成员变量使用了“Inject”注解修饰
                        memberProperty.isAccessible = true//修改访问权限为"public"
                        memberProperty.javaField?.set(obj, memberProperty.javaField?.type?.newInstance())//为该字段生成实例,并重新赋值
                    }
                }
            }
        }
    }
}

上面就是我们的中间者实现代码,代码中的注释已经比较详细,这里不再展开。下面看下如何使用:

//Color类,保持不变
class Color {
    fun getColor(): String {
        return "red"
    }
}
//画笔类
class Paint {
//注意这里!我们不再直接生成Color对象,而是使用Inject注解
//进行了修饰,意思是交给我们的"中间者"来将其实例化
//由于无法立即赋值,所以需要使用lateinit关键字修饰,表明该字段会
//在合适的时候进行实例化
    @Inject
    private lateinit var color: Color
  
    fun draw() {
        println("draw with color: " +color.getColor())
    }
}
//测试代码
fun main(args: Array<String>) {
    val paint = Paint()
    Ioc.inject(paint, paint::class)//这里我们将实例化的控制权交给了Ioc
    paint.draw()//打印'draw with color: red'
}

至此,本篇文章已经阐述完毕。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容