kotlin学习笔记之注解与反射

一、声明并应用注解

         一个注解允许你把额外的元数据关联到一个声明上。然后元数据就可以被相关的源代码工具访问,通过编译好的类文件或是在运行时,取决于这个注解是如何配置的

        1、应用注解

        在kotlin中使用注解的方法和java一样。要应用一个注解,以@字符作为(注解)名字的前缀,并放在要注解的声明最前面。可以注解不同的代码元素,比如函数和类。

        例如,如果你正在使用JUunit框架,可以用@Test标记一个测试方法:

        我们再来看一个更有趣的例子,@Deprecated注解。它在kotlin中的含义和java一样,但是kotlin用replaceWith参数增强了它,让你可以提供一个替代着的(匹配)模式,以支持平滑过渡到API的新版本。下面这个例子向你展示了如何给该注解提供实参(一条不推荐使用的消息和一个替代者的模式):         实参在括号中传递,就和常规函数的调用一样。用了这种声明之后,如果有人使用了remove函数,IDEA不仅会提示应该使用哪个函数来代替它(这个例子中是removeAt),还会提供一个自动的快速修正。

        注解只能拥有如下类型的参数:基本数据类型、字符串、枚举、类引用、其他的注解类,以及前面这些类型的数组。指定注解实参的语法和java有些微小的差别:

         注解实参需要在编译期是已知的,所以你不能引用任意的属性作为实参。要把数据当做注解实参使用,你需要使用const修饰符来标记它,来告知编译器这个属性时编译期常量。下面是一个JUnit @Test注解的例子,使用timeout参数指定测试超时时长,单位为毫秒:

        正如3.3.1节讨论过的,用const标注的属性可以声明在一个文件的顶层或者一个object之中,而且必须初始化为基本数据类型或者String类型的值。如果你尝试使用普通属性作为注解实参,将会得到一个错误“Only 'const val' can be used in constant expression” 。

        2、注解目标

        许多情况下,kotlin源代码中的单个声明会对应成多个java声明,而且它们每个都能携带注解。例如,一个kotlin属性就对应了一个java字段、一个getter,以及一个潜在的setter和它的参数。而一个主构造方法中声明的属性还多拥有一个对应的元素:构造方法的参数。因此,说明这些元素中哪些需要注解十分必要。

        使用点目标声明被用来说明要被注解的元素。使用点目标被放在@符号符号和注解名称之间,并用冒号和注解名称隔开。下图中单词get导致注解@Rule被应用到了属性的getter上。

         下面我们来看一个使用这个注解的例子。在JUnit中可以指定一个每个测试方法被执行之前都会执行的规则。例如,标准的TemporaryFolder规则用来创建文件和文件夹,并在测试结束后删除它们。

        要指定一个规则,在java中需要声明一个用@Rule注解的public字段或者方法。如果在你的kotlin测试类中只是用@Rule注解了属性folder,你会得到一个JUnit异常:“The (???) ‘folder’ must be public.”((???) 'folder'必须是公有的)。这是因为@Rule被应用到了字段上,而字段默认是私有的。要把它应用到(公有的)getter上,要显式地写出来:@get:Rule,就像下面这样:

        如果你使用Java中声明的注解来注解一个属性,它会被默认地应用到相应的字段上。kotlin也可以让你声明被直接对应到属性上的注解。

        kotlin支持的使用点目标的完整列表如下: 

        任何应用到file目标的注解都必须放在文件的顶层,放在package指令之前@JvmName是常见的应用到文件的注解之一,它改变了对应类的名称。3.2.3节中已经展示了一个例子:@file:JvmName("StringFunctions")。

         注意,和java不一样的是,kotlin允许你对任意的表达式应用注解,而不仅仅是类和函数的声明及类型。最常见的例子就是@Suppress注解,可以用它来抑制被注解的表达式的上下文中的特定的编译器警告。下面就是一个注解局部变量声明的例子,抑制了未受检转换的警告:

        注意,在IDEA中,在出现这个编辑器警告的地方,按下Alt + Enter组合键并从意向选项菜单中选择Suppress(抑制),IDEA就会帮你插入这个注解。 

3、使用注解定制JSON序列化

         注解的经典用法之一就是定制化对象的序列化序列化就是一个过程,把对象转换成可以存储或者在网络上传输的二进制或者文本的表示法。它的逆变过程,反序列化,把这种表示法转换回一个对象。而最常见的一种用来序列化的格式就是JSON。已经有很多广泛使用的库可以把java对象序列化成JSON,包括Jackson(https://github.com/FasterXML/jackson)和GSON(https://github.com/google/gson)。就和任何其他java库一样,它们和kotlin完全兼容。

        在本章中,我们将会讨论一个满足此用途的名为JKid的纯Kotlin库。它足够小巧,你可以轻松地读完它的全部源码,我们也鼓励你在阅读本章的同时阅读它的源码。

        让我们从最简单的例子开始,测试一下这个库:序列化和反序列化一个Person类的实例。把实例传给serialize函数,然后它就会返回一个包含该实例JSON表示法的字符串: 

        一个对象的JSON表示法由键值对组成:具体实例的属性名称和它们的值之间的键值对,比如:“age”:29。

        要从JSON表示法中取回一个对象,要调用deserialize函数:

        当你从JSON数据中创建实例的时候,必须显式地指定一个类作为类型参数,因为JSON没有存储对象的类型。这种情况下,你要传递Person类。

        下图展示了一个对象和它的JSON表示法之间的等价关系。注意序列化之后的类能包含的不仅是图中展示的这些基本数据类型或者字符串类型的值,还可以是集合,以及其他值对象类的实例。

         你可以使用注解来定制对象序列化和反序列化的方式。当把一个对象序列化成JSON的时候,默认情况下这个库尝试序列化所有属性,并使用属性名称作为键。注解允许你改变默认的行为,这一节我们会讨论两个注解:@JsonExclude和@JsonName。本章稍后你就会看到它们的实现。

        参考下面这个例子:

         你注解了属性firstName,来改变在JSON中用来表示它的键。而属性age也被注解了,在序列化和反序列化的时候会排除它。注意,你必须指定属性age的默认值。否则,在反序列化时你无法创建一个Person的新实例。下图展示了Person类实例的表示法发生了怎样的变化。

 4、声明注解

        在这一节,你会以JKid库中的注解为例学习怎样声明它们。注解@JsonExclude有着最简单的形式,因为它没有任何参数:

annotation class JsonExclude

        语法看起来和常规类的声明很像,只是在class关键字之前加上了annotation修饰符。因为注解类只是用来定义关联到声明和表达式的元数据的结构,它们不能包含任何的代码。因此,编译器禁止为一个注解类指定类主体

        对拥有参数的注解来说,在类的主构造方法中声明这些参数:

annotation class JsonName(val name: String)

        你用的是常规的主构造方法的声明语法。对一个注解类的所有参数来说,val关键字是强制的

        作为对比,下面是如何在java中声明同样的注解:

/* java */
public @interface JsonName {
    String value();
}

        注意,java注解拥有一个叫做value的方法,而kotlin注解拥有一个name属性。java中的value方法很特殊:当你应用一个注解时,你需要提供value以外所有指定特性显式名称。而另一方面,在kotlin中应用注解就是常规的构造方法调用。可以使用命名实参语法让实参的名称变成显式的,或者可以省略掉这些实参的名称:@JsonName(name = "first_name")和@JsonName("first_name")含义一样,因为name是JsonName构造方法的第一个形参(它的名称可以省略)。然而,如果你需要把java声明的注解应用到kotlin元素上,必须对除了value以外的所有实参使用命名实参语法,而value也会被kotlin特殊对待

5、元注解:控制如何处理一个注解

        和java一样,一个kotlin注解类自己也可以被注解。可以应用到注解类上的注解被称为元注解。标准库中定义了一些元注解,它们会控制编译器如何处理注解。其他一些框架也会用到元注解——例如,许多依赖注入库使用了元注解来标记其他注解,表示这些注解用来识别有同样类型的不同的可注入对象。

        标准库定义的元注解中最常见的就是@Target。JKid中@JsonExclude和@JsonName的声明使用它为这些注解指定有效的目标。下面展示了它是如何应用(在注解上)的:        

        @Target元注解说明了注解可以被应用的元素类型。如果不使用它,所有的声明都可以应用这个注解。这并不是JKid想要的,因为它只需要处理属性的注解。

        AnnotationTarget枚举的值列出了可以应用注解的全部可能的目标。包括:类、文件、函数、属性访问器、所有的表达式等等。如果需要,你还可以声明多个目标:@Target(AnnotationTarget.CLASS,  AnnotationTarget.METHOD) 

        要声明你自己的元注解,使用ANNOTATION_CLASS作为目标就好了

        注意,在java代码中无法使用目标为PROPERTY的注解:要让这样的注解可以在java中使用,可以给它添加第二个目标AnnotationTarget.FIELD。这样,注解既可以应用到kotlin中的属性上,也可以应用到java中的字段上。

6、使用类做注解参数

         你已经见过了如何定义保存了作为其实参的静态数据的注解,但有时候你有不同的需求:能够引用类作为声明的元数据。可以通过声明一个拥有类引用作为形参的注解类来做到这一点。在JKid库中,这出现在@DeserializeInterface注解中,它允许你控制那些接口类型属性的反序列化。不能直接创建一个接口的实例,因此需要指定反序列化时那个类作为实现被创建。

        下面这个简单例子展示了这个注解如何使用:

        当JKid读到一个Person类实例嵌套的company对象时,它创能并反序列化了一个CompanyImpl的实例,把它存储在company属性中,使用CompanyImpl::class作为@DeserializeInterface注解的实参来说明这一点。通常,使用类名称后面跟上::class关键字来引用一个类。 

        现在我们看看这个注解是如何声明的。它的单个实参是一个类引用,就像@DeserializeInterface(CompanyImpl::class):

annotation class DeserializeInterface(val targetClass: KClass<out Any>)

        KClass是java的java.lang.Class类型在kotlin中的对应类型。例如,CompanyImpl::class的类型是KClass<CompanyImpl>,它是这个注解形参类型的子类型,如下图所示:

        如果你只写出KClass<Any>而没有out修饰符,就不能传递CompanyImpl::class作为实参:唯一允许的实参是Any:class。out关键字说明允许引用那些继承Any的类,而不仅仅是引用Any自己

7、使用泛型类做注解参数

        默认情况下,JKid把非基本数据类型的属性当成嵌套的对象序列化。但是你可以改变这种行为并为某些值提供你自己的序列化逻辑。

        @CustomSerializer注解接收一个自定义序列化器类的引用作为实参。这个序列化器类应该实现ValueSerializer接口:

        

        假设你需要支持序列化日期,而且已经为此创建了你自己的DateSerializer类,它实现了ValueSerializer<Date>接口(这个类是JKid源码中的一个例子)。下面展示如何在Person类上应用它:

        现在我们来看看CustomSerializer注解是如何声明的。 ValueSerializer类是泛型的而且定义了一个类型形参,所以在你引用该类型的是需要提供一个类型实参值。因为你不知道任何关于那些应用了这个注解的属性类型的信息,可以星号投射作为(类型)实参:

        下图审视了serializerClass参数的类型并解释了其中不同的部分。你需要保证注解只能引用实现了ValueSerializer接口的类。例如@CustomSerializer(Date::class)的写法是不允许的,因为Date没有实现ValueSerializer接口。

        是不是很麻烦?好消息是每一次需要使用类作为注解实参的时候都可以应用同样的模式。可以这样写KClass<out YourClassName> 。如果YourClassName有它自己的类型实参,就用*代替它们。

二、反射:在运行时对Kotlin对象进行自省

        反射是,简单来说,一种在运行时动态地访问对象属性和方法的方式,而不需要事先确定这些属性时什么。一般来说,当你访问一个对象的方法或属性时,程序的源代码会引用一个具体的声明,编译器将静态地解析这个引用并确保这个声明是存在的。但有些时候,您需要编写能够使用任意类型的对象的代码,或者只能在运行时才能确定要访问的方法或属性的名称。JSON序列化库就是这种代码绝好的例子:它要能够把任何对象都序列化成JSON,所以它不能引用具体的类和属性。这时该反射大显身手了。

        当在kotlin中使用反射时,你会和两种不同的反射API打交道。第一种是标准的java反射,定义在java.lang.reflect中。因为kotlin类会被编译成普通的java字节码,java反射API可以完美地支持它们。实际上,这意味着使用了反射API的java库完全兼容kotlin代码。

        第二种是kotlin的反射API,定义在kotlin.reflect中。它让你能够访问那些在java世界里不存在的概念,诸如属性和可空类型。但这一次它没有为java反射API提供一个面面俱到的替身,而且不久你就会看到,有些情况下你仍然会回去使用java反射。这里有个重要的提示,kotlin反射api没有仅限于kotlin类:你能够使用同样的api访问任何jvm语言写成的类。

1、 kotlin反射API:KClass、KCallable、KFunction和KProperty

        kotlin反射API的主要入口就是KClass,它代表了一个类。KClass对应的是java.lang.class,可以用它列举和访问类中包含的所有声明,然后是它的超类中的声明,等等。MyClass::class的写法会带给你一个KClass的实例。要在运行时取得一个对象的类,首先使用javaClass属性获得它的java类,这直接等价于java的java.lang.Object.getClass()。然后访问该类的.kotlin扩展属性,从java切换到kotlin的反射API。        

         这个简单的例子打印出了类的名称和它的属性的名称,并且使用.memberProperties来收集这个类,以及它的所有超类中定义的全部非扩展的属性。

        如果浏览一下KClass的声明,你会发现它包含了大量方便的方法,用于访问类的内容:

        KClass的许多有用的特性,包括前面例子中用到的memberProperties,都声明成了扩展。

        你可能已经发现了由类的所有成员组成的列表是一个KCallable实例的集合。KCallable是函数和属性的超接口。它声明了call方法,允许你调用对应的函数或对应属性的getter:

        你把(被引用)函数的实参放在varargs列表中提供给它。下面的代码展示了如何通过反射使用call来调用一个函数:

        在5.1.5节中你见过::foo的语法,现在你可以发现这个表达式的值是来自反射API的KFunction类的一个实例。你会使用KCallable.call方法来调用被引用的函数。这个例子中,你需要提供一个单独的实参,42。如果你用错误数量的实参去调用函数,比如kFunction.call(),这将会抛出一个运行时异常”IllegalArgumentException: Callable expects 1 arguments,but 0 were provided“。

        然而,这种情况下,你可以用一个更具体的方法来调用这个函数。::foo表达式的类型是KFunction1<Int, Unit>,它包含了形参类型和返回类型的信息。1表示这个函数接收一个形参使用invoke方法通过这个接口来调用函数。它接收固定数量的实参(这个例子中是1个),而且这些实参的类型对应着KFunction1接口的类型形参。您也可以直接调用kFunction:

        现在你无法用数量不正确的实参去掉头kFunction的invoke方法:这连编译都不能通过因此你有这样一个具体类型的KFunction,它的形参类型和返回类型是确定的,那么应该优先使用这个具体类型的invoke方法。call方法是对所有类型都有效的通用手段,但是它不提供类型安全性。 

         你也可以在一个KProperty实例上调用call方法,它会调用该属性的getter。但是属性接口为你提供了一个更好的获取属性值的方式:get方法。

        要访问get方法,你需要根据属性声明的方式来使用正确的属性接口。顶层属性表示为KProperty0接口的实例,它有一个无参数的get方法:

        一个成员属性由KProperty1的实例表示,它拥有一个单参数的get方法。要访问该属性的值,必须提供你需要的值所属的那个对象实例。下面这个例子在memberProperty变量中存储了一个指向属性的引用;然后调用memberProperty.get(person)来获取属于具体person实例的这个属性的值。所以memberProperty指向了Person类的age属性,memberProperty.get(person)就是动态获取person.age的值的一种方式:

        注意,KProperty1是一个泛型类。变量 memberProperty的类型是KProperty<Person, Int>,其中第一个类型参数表示接收者的类型,而第二个类型参数代表了属性的类型。这样你只能对正确类型的接收者调用它的get方法;而memberProperty.get("Alice")这样的调用不会通过编译。

        还有一点值得注意,只能使用反射访问定义在最外层或者类中的属性,而不能访问函数的局部变量。如果你定义了一个局部变量x并试图使用::x来获取它的引用,你会得到一个编译期错误:”References to variables aren't supported yet“。

        下图展示了运行时你可以用来访问源码元素的接口的顶级结构。因为所有的声明都能被注解,所以代表运行时声明的接口,比如KClass、KFucntion和KParameter,全部继承了KAnnotatedElement。KClass既可以用来表示类也可以用来表示对象。KProperty可以表示任何属性,而它的子类KMutableProperty表示一个用var声明的可变属性。可以使用声明在KProperty和KMutableProperty中的特殊接口Getter和Setter,把属性的访问器当成函数使用——例如,如果你需要取回它们的注解。两个访问器的接口都继承了KFunction。简单起见,图中我们省略了KProperty0这样的具体的属性接口。

 2、用反射实现对象序列化

        首先,我们回忆一下JKid中序列化函数的声明:

fun serialize(obj: Any): String

        这个函数接收一个对象然后返回JSON表示法的字符串。它通过一个StringBuilder实例来构建JSON的结果。这个函数在序列化对象属性和它们的值的同时,这些内容会被附加到这个StringBuilder对象之中。我们把实现放在StringBuilder的扩展函数中,好让append的调用更加简洁。这样,你不用限定符就可以方便地调用append方法:

        把一个函数参数转化成一个扩展函数的接收者是kotlin代码中常见模式。注意serializeObject没有拓展StringBuilder的API。它执行的操作在这个特殊的上下文之外没有任何意义,所以它被标记成private,以保证它不会在其他地方使用。它被声明成扩展以强调这个特殊对象是代码块的主要对象,让这个对象用起来更容易。

        结果,serialize函数把所有的工作委托给了 serializeObject:

        buildString会创建一个StringBuilder,并让你在lambda中填充它的内容。这个例子中,对 serializeObject(obj)的调用提供了要填充的内容。

        现在我们来讨论一下序列化函数的行为。默认情况下,它将序列化对象的所有属性:基本数据类型和字符串都会被酌情序列化成JSON数值、布尔值和字符串值;集合将会被序列化成JSON数组;其他类型的属性将会被序列化成嵌套的对象。正如我们在上一节中讨论的,这种行为可以通过注解进行定制。

        我们来看看serializeObject的实现,在这里可以在真实的场景中观察反射API。

        这个函数的实现应该很清晰:逐一序列化类的每一个属性。生成的JSON看起来会是这样:{prop1:value1, prop2:value2} 。joinToStringBuilder函数保证属性与属性之间用逗号隔开。serializeString函数按照JSON格式的要求对特殊的字符进行转义。serializeString函数检查一个值是否是一个基本数据类型的值、字符串、集合或是嵌套对象,然后相应地序列化它的内容。

        在前面小节中,我们讨论过一种获取KProperty势力值的方式:get方法。在那个例子中,你使用过类型为KProperty1<Person, Int>的成员引用Person::age,它让编译器知道可接收者和属性值的确切类型。然而在这个例子中,确切类型是未知的,因为你列举了一个对象的类中所有属性。因此,prop变量拥有类型KProperty<Any, *>,而prop.get(obj)返回一个Any类型的值。你不会得到任何针对接收者的编译期检查,但是因为你传递的对象和获取属性列表的对象是同一个,接收者的类型是不会错的。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值