前言:日日行,不怕千万里;常常做,不怕千万事。
一、概述
目前来说,你已经见过许多有关类和函数的特性,但是它们全部要求在使用这些类和函数的时候说明它们的确切名称,作为程序代码的一部分。要调用一个函数,你需要知道它定义在哪个类中,还有它的名称和参数类型。注解有超越这个规则的能力,并让你编写出使用事先未知的任意类的代码。可以使用注解赋予这些类库特定的语义。
使用注解非常直接了当,但编写你自己的注解尤其是编写处理它们的代码,就没有这么简单了。使用注解的语法和 Java 完全一样,而声明自己注解类的语法却略有不同。下面来详细介绍应用和定义注解,怎样让你定制具体的类和属性被某个库序列化和反序列化的。
绝大多数的 Java 框架都使用了注解,我们也经常用到,Kotlin 的注解的核心概念也是一样的。一个注解允许你把额外的元数据关联到一个声明上,然后元数据就可以被相关的源代码工具访问,通过编译好的类文件或是在运行时,取决于这个注解是如何配置的。
本文导图:
二、声明和应用注解
2.1 声明注解
要声明注解,在类 Class 关键字前面加上注解修饰符 annotation
。因为注解类只是用来定义关联声明和表达式的元数据的结构,它们不能包含任何代码。所以,编译器禁止为一个注解类指定主体。
//声明注解,极简形式
annotation class Suspendable
(1)对于拥有参数的注解来说,在注解类的主构造方法中声明这些参数(使用的是常规的主构造函数的声明语法,对于一个注解类的所有参数来说,val
关键字是必须强制的):
annotation class Animal(val name: String)
(2)对比下,如何在 Java 中声明同样的注解:
// Java
public @interface AnnNormal {
String value();
}
(3)如果你需要注解一个类的主构造函数,你需要在构造函数声明中添加 constructor
关键字,并且它之前添加注解:
//给 Mouse 类添加注解 @Animal
class Mouse @Animal("m") constructor(val name: String) { }
(4)注解也可以在 lambdas 上使用,他们将被应用到 invoke()
方法中,lambdas 的主体将生成在该方法中。这对于使用注解进行并发控制的 Quasar 等框架非常有用。
annotation class Suspendable
val light = @Suspendable {
// Fiber.sleep(10)
}
(5)如果有多个注解与同一个目标,可以通过 @set:[]
集合的形式将所有注解放在括号内来避免重复目标:
class Teacher {
@set:[Suspendable Animal("no")]//注解 Suspendable 和 Animal
var name: String = ""
}
(6)注意:在 Kotlin 中应用注解就是常规的构造方法调用。可以使用命名实参语法让实参的名称变成显式的。如果你需要把 Java 中的注解声明到 Kotlin 元素上,必须对所有实参使用命名实参语法。Java 中由于没有定义编写的注解的参数顺序,所以不能使用常规函数调用语法来传递参数。
// Java
public @interface AnnNormal {
int age();
String name();
}
使用命名参数调用:
//kotlin
@AnnNormal(age = 10, value = "Kotlin")
class Chicken {}
2.2 应用注解
在 Kotlin 中使用注解的方法和 Java 一样。要引用一个注解,以 @
字符作为(注解)名字的前缀,并放在你要注解的声明最前面。可以注解不同的代码元素,比如函数和类。
(1)例如,使用 JUnit 框架(项目中已导入 JUnit 相关jar包),使用 @Test
标记一个方法:
@Test fun testMethod() {
Assert.assertTrue(true)
}
其中 @Test
注解指引 JUnit 框架把 testMethod()
这个方法当测试调用。
(2)比如 @Deprecated
注解,它在 Kotlin 中含义和 Java 的一样。(如果一个注解用做另一个注解的参数,它的名称不带 @ 字符前缀,如下面的 ReplaceWith
注解)
@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
)
但是 Kotlin 中使用 replaceWith 参数增强了它,让你可以提供一个替代者的匹配模式,以支持平滑地过度到 API 的新版本。下面展示了如果给该注解提供实参(一个不推荐使用该函数的消息提示和一个替代者的模式):
@Deprecated("Use removeAt(index) instead", ReplaceWith("removeAt(index)"))
fun remove(index: Int) {
//TODO
}
//替代方法
fun removeAt(index: Int) {
//TODO
}
实参就在括号中传递,就和常规函数的调用一样。如上面使用注解声明了以后,有人调用了 remove(index: Int)
函数后,会提示使用那个函数来代替它(上面是 removeAt),还会提供一个自动的快速修正。
remove(0)
调用 remove(0)
,可以看到方法被声明为过时,并且提示:Use removeAt(index) instead
,你可以使用 Android studio 快捷键 Alt+Enter
选择 removeAt(index)
快速修正:
(3)注解构造函数参数类型只能拥有如下几种类型:基本数据类型,字符串,枚举,类引用,其他的注解类,以及前面这些类型的数组。指定注解实参的语法与 Java 有一点差别:
- 要把一个类指定为注解实参,在类名后加上,格式:
类名::class
,比如:@Test MyClass::class
; - 要把一个注解指定为另一个注解的实参,去掉注解名称前面的
@
符号,比如上面例子中的 ReplaceWith 是一个注解,你将它指定为@Deprecated
的实参时没有@
符号; - 要把一个数组指定为一个注解的实参,使用
arrayOf()
函数:@RequestMapping(path = arrayOf("/foo", "/bar"))
。如果注解类是在 Java 中声明的,命名为 value 的形参按需自动地被转换成可变长度的形参,所以不用arrayOf()
函数就可以提供多个实参。
注意:注解参数不能具有可为空的类型,因为JVM不支持将 null
存储为注解属性的值。
注解实参需要在编译期就是已知的,所以你不能引用任意的属性作为实参。如果你要把属性当做注解实参使用,这个属性需要使用 const
关键字修饰,来告知编译器它是编译时常量。下面使用 JUnit @Test
注解,使用 timeout 指定测试超时时长,单位为毫秒:
const val TIMEOUT = 100L//顶层
@Test(timeout = TIMEOUT)
fun testMethod() {
Assert.assertTrue(true)
}
使用 const
修饰的属性可以标注在文件顶层或者一个 object 中,而且必须初始化为基本数据类型和String类型的值,如果你使用普通属性作为注解实参,将会报错:注解参数必须是编译时常量。
2.3 注解目标
很多情况下,Kotlin 源代码中的单个声明对应多个 Java 声明,而且它们每个都能携带注解。一个 Kotlin 属性就对应一个 Java 字段,一个 getter,以及潜在的 setter 和它的参数。而一个主构造函数中声明的属性还多对应的元素:构造方法的参数。因此说明那些元素需要注解是非常有必要的。
使用点目标声明被用来说明要注解的元素。点目标放在 @
符号和注解名称之间,并用冒号 :
和注解名称隔开,如下面的点目标 get
,导致注解 @Rule
被应用到了属性的 getter 上面:
(1)使用例子:在 JUnit 中可以指定每一个测试方法执行之前都会执行的规则。标准的 TemporaryFolder 规则用来创建文件和文件夹,并在测试结束后删除他们。要指定一个规则,在 Java 中需要声明一个用 @Rule
注解的 public 字段或者方法。如果你在 Kotlin 测试类中只是用 @Rule
注解了属性 folder,要把它应用到 public 的 getter 上,要显式地写出来 @get:Rule
,如下:
class AnnotationsActivity{
//@Rule注解应用到属性 getter 中,注解的是 getter 而不是属性
@get:Rule
val folder = TemporaryFolder()
@Test
fun testTemporaryFolder() {
val file = folder.newFile("myFile.txt")
}
}
如果你使用 Java 中声明的注解来注解一个属性,它会被默认地应用到相应的字段上,Kotlin 也可以让你声明被直接对应到属性上的注解:
file
:包含在文件中声明的顶层函数和属性的类;property
:带有此目标的注解是对 Java 不可见的;field
:为属性生成的字段;get
:属性的 getter;set
:属性的 setter;receiver
:扩展函数或扩展属性的接收者参数;param
:构造函数参数;setparam
:属性 setter 的参数;delegate
:为委托属性存储委托实例的字段。
任何使用到 file
目标的注解都应该放到文件顶层,放在 package 指令之前。@JvmName
是比较常见的应用到文件的注解,
@file:JvmName("foo")
package com.suming.kotlindemo.blog
注意:和 Java 不一样,Kotlin 允许你对任意的表达式使用注解,而不仅仅是类和函数的声明以及类型。
(2)例如:@Suppress
注解,可以用它来抑制被带注解的元素的编译警告。下面是一个注解局部变量声明的例子,抑制了未受检测转换的警告:
fun suppressMethod(list: List<*>) {
//代码黄色警告Unchecked_cast:List<*> to List<String>
val strings = list as List<String>
//警告消失,被抑制
@Suppress("Unchecked_cast")//names 表示要抑制的编译器诊断程序的名称
val strings2 = list as List<String>
}
温馨提示:在 Android studio 中出现这个警告按下 Alt+Enter
组合键选择 Suppress
抑制,会帮你插入这个注解。如下图:
用注解控制 Java API
Kotlin 提供了各种注解来控制 Kotlin 编写的声明如何编译成字节码并暴露给 Java 调用者。其中一些注解代替了 Java 语言中对应的关键字,例如注解
@Volatile
和@Strictfp
直接充当了 Java 关键字 Volatile 和 Strictfp 的替身。其他的注解则是用来改变 Kotlin 声明对 Java 调用者的可见性:
@JvmName
:将改变由 Kotlin 生成的 Java 方法或字段名称;@JvmStatic
:能被用在对象声明或伴生对象的方法上,把它们暴露成 Java 的静态方法;@JvmOverloads
:指定 Kotlin 编译器为带默认参数值的函数生成多个重载(函数);@JvmField
:可以应用于一个属性,把这个属性暴露成一个没有访问器的公有 Java 字段。
2.4 元注解:控制如何处理一个注解
和 Java 一样,Kotlin 注解类自己也可以被注解。可以应用到注解类上的注解被称为元注解。标准库中定义了一些元注解,它们会控制编译器如何处理注解。许多依赖注入库使用了元注解来标记其他注解,表示这些注解可以用来识别拥有同样类型的不同的可注入对象。
标准库中定义的元注解最常见的的是 @Target
,下面使用它为一些注解指定有效目标,来看看它是如何应用到注解上的:
@Target(AnnotationTarget.PROPERTY)
annotation class Person
@Target
元注解说明了注解可以被应用的元素类型。如果不使用它,所有的声明都可以应用这个注解,但是 @Person
注解被指定为只需处理属性注解(AnnotationTarget.PROPERTY
)。AnnotationTarget 枚举的值列出了可以应用注解的全部可能的目标,包括:类、文件、函数、属性、属性访问器、所有表达式等等,如果需要你还可以声明多个目标 @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
。
要声明自己的元注解,使用 AnnotationTarget.CLASS
作为目标就可以了。
//元注解
@Target(AnnotationTarget.CLASS)
annotation class AnnBinding
@AnnBinding
annotation class MyBanding//注解 MyBanding 应用元注解 AnnBinding
注意:在 Java 代码中无法使用目标为 PROPERTY 的注解,需要给它添加第二个目标 AnnotationTarget.FIELD
才可以在 Java 中使用,这样注解既可以应用到 Kotlin 属性上,也可以应用到 Java 字段上。
注解的其他属性可以用元注解 annotation
类来指定:
- @Target: 指定可能被注解的元素类型(类,函数,属性,表达式等);
- @Retention: 指定注解是否存储在编译后的类文件中,以及是否在运行时通过反射可见(默认情况下,两者都为true),Java 默认在
.class
文件中保留注解但不会让它们在运行时被访问到。大多数注解确实需要在运行时存在,Kotlin 的默认行为不同:注解拥有 RUNTIME 保留期; - @Repeatable: 允许再单个元素上多次使用相同的注解;
- @MustBeDocumented: 指定注解是公共API的一部分,并且应该包含在生成的API文档中显示的类或方法签名中。
三、注解参数
3.1 使用类做注解参数
前面知道如何定义保存了作为其实参的静态数据的注解,你还可以能够引用类作为声明的元数据。通过声明一个拥有类引用作为形参的注解来实现。比如 @DeserializeInterface
注解中它允许你控制那些接口类型属性的反序列化。不能直接创建一个接口的实例,因此需要指定反序列化时哪个类作为实现被创建。
annotation class DeserializeInterface(val cla: KClass<out Any>)
读到一个 Student 类实例嵌套的 company 对象时,它创建并反序列化一个 CompanyImpl 的实例,并把它存储在 company 属性中。使用 CompanyImpl::class
来作为 @DeserializeInterface
注解的实参说明了这一点。(使用 类名::class
关键字表示引用一个类)
interface Company {
val name: String
}
data class CompanyImpl(override val name: String) : Company
data class Student(
val name: String,
@DeserializeInterface(CompanyImpl::class) val company: Company
)
KClass 类似 Java 的 java.lang.Class,它用来保存 Kotlin 类的引用。KClass 的类型参数说明这个引用可以指向哪些 Kotlin 类,上面的 CompanyImpl::class(KClass<CompanyImpl>)
是注解形参类型(KClass<out Any>
)的子类型。
注意:KClass<out Any>
需要使用 out 修饰符,否则不能传递 CompanyImpl::class
作为实参,唯一允许的实参将是 Any::class
,out 关键字允许引用那些继承 Any 的类,而不仅仅是引用 Any 自己。
3.2 使用泛型类做注解参数
有时候会把非基本数据类型的属性当做嵌套的对象序列化,其实可以改变这种行为,并为某些值提供你自己的序列化逻辑。
下面的 @CustomSerializer
注解接收一个自定义序列化器类的引用作为实参,这个序列化器应该实现 ValueSerializer 接口:
interface ValueSerializer<T> {
fun toJsonValue(value: T): Any?
fun fromJsonValue(jsonValue: Any): T?
}
//注解
annotation class CustomSerializer(
val cla: KClass<out ValueSerializer<*>>
)
@CustomSerializer
注解是如何声明的? ValueSerializer 接口是泛型的而且定义了一个类型形参,你在引用该类型的时候需要提供一个类型实参值,但是你不知道那些应用了这个注解的属性类型的信息,可以使用星号投射作为类型实参(*
)。
假设你需要支持序列化日期,而且为此创建了 DataSerializer 类,它实现了 ValueSerializer<Date>
接口
data class Rabbit(
val name: String,
@CustomSerializer(DataSerializer::class) val data: ContactsContract.Data
)
你需要保证注解只能引用实现了 ValueSerializer 接口的类,比如上面的 @CustomSerializer(Data::class)
写法是不允许的,因为 Date 没有实现 ValueSerializer 接口。
SerializerClass 注解参数的类型,指向 ValueSerializer 实现类的类引用将会是有效的注解实参
是不是很麻烦?好处是每一次需要使用类作为注解实参的时候都可以应用同样的模式,可以这样写 KClass<out className>
如果 className 有自己的类型实参则可以使用 * 代替。
四、总结
源码地址:https://github.com/FollowExcellence/KotlinDemo-master
点关注,不迷路
好了各位,以上就是这篇文章的全部内容了,能看到这里的人呀,都是人才。
我是suming,感谢各位的支持和认可,您的点赞就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !
要想成为一个优秀的安卓开发者,这里有必须要掌握的知识架构,一步一步朝着自己的梦想前进!Keep Moving!。