一、声明并应用注解
一个注解允许你把额外的元数据关联到一个声明上。然后元数据就可以被相关的源代码工具访问,通过编译好的类文件或是在运行时,取决于这个注解是如何配置的。
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来调用一个函数: