Kotlin 和 Java 反射
- 1)Kotlin 的
KClass
和 Java 的Class
可以看作同一个含义的类型,并且可以通过.java
和.kotlin
方法在KClass
和Class
之间互相转化。 - 2)Kotlin 的
KCallable
和 Java 的AccessiableObject
都可以理解为可调用元素。Java 中构造方法为一个独立的类型,而 Kotlin 则统一作为KFunction
处理。 - 3)Kotlin 的
KProperty
和 Java 的Field
不太相同。Kotlin 的KProperty
通常指相应的Getter
和Setter
(只有可变属性Setter
)整体作为一个KProperty
(通常情况 Kotlin 并不存在字段的概念),而 Java 的Field
通常仅仅指字段本身。
在某些情况下(通常是碰到一些Kotlin独有的特性时)Kotlin编译器会在生产的字节码中存储额外信息,这些信息目前是通过 kotlin.Metadata
解实现的。Kotlin 编译器会用 Metadata
标注这些类。
Kotlin 的 KClass
KClass
的特别属性或者函数(在Kotlin中独有,Java没有与之对应的特性):
属性或函数名称 | 含义 |
---|---|
isCompanion | 是否伴生对象 |
isData | 是否数据类 |
isSealed | 是否密封类 |
objectInstance | object 实例(如果是object ) |
companionObjectInstance | 伴生对象实例 |
declaredMemberExtensionFunctions | 扩展函数(声明的) |
declaredMemberExtensionProperties | 扩展属性(声明的) |
memberExtensionFunctions | 本类及超类扩展函数 |
memberExtensionProperties | 本类及超类扩展属性 |
starProjectedType | 泛型通配类型 |
Kotlin 的 KCallable
Kotlin 把 Class
中的属性(Property
)、函数(Funciton
)甚至构造函数都看作 KCallable
,因为它们是可调用的,它们都是Class
的成员。那我们如何获取一个Class
的成员呢?
KClass
给我们提供了一个members
方法,它的返回值就是一个Collection<KCallable<*>>
。
KCallable
提供的 API:
API 描述 | 含义 |
---|---|
isAbstract: Boolean | 此 KCallable 是否为抽象的 |
isFinal: Boolean | 此 KCallable 是否为 final 的 |
isOpen: Boolean | 此 KCallable 是否为 open 的 |
name: String | 此 KCallable 的名称 |
parameters: List<KParameter> | 调用此 KCallable 需要的参数 |
returnType: KType | 此 KCallable 的返回类型 |
typeParameters: List<KTypeParameter> | 此 KCallable 的类型参数 |
visibility: KVisibility? | 此 KCallable 的可见性 |
call(vararg args: Any?): R | 给定参数调用此 KCallable |
KMutableProperty
是KProperty
的一个子类,那我们如何识别一个属性是KMutableProperty
还是KProperty
呢?参考如下代码:
fun KMutablePropertyShow() {
val p = Person("张三", 8, "HangZhou")
val props = p::class.memberProperties
for (prop in props) {
when (prop) {
is KMutableProperty<*> -> prop.setter.call(p, "Hefei")
else -> prop.call(p)
}
}
println(p.address)
}
获取参数信息
Kotlin 把参数分为3个类别,分别是函数的参数(KParameter
)、函数的返回值(KType
)及类型参数(KTypeParameter
)。
KParameter
使用KCallabel.parameters
即可获取一个List<KParameter>
,它代表的是函数(包括扩展函数)的参数。
API 描述 | 含义 |
---|---|
index: Int | 返回该参数在参数列表里面的索引 |
isOptional: Boolean | 该参数是否为 Optional |
isVararg: Boolean | 该参数是否为 vararg |
kind: Kind | 该参数的 Kind |
name: String? | 该参数的名称 |
type: KType | 该参数的类型 |
fun KParameterShow() {
// val p = Person("张三", 8, "HangZhou")
for (c in Person::class.members) {
print("${c.name} -> ")
for (p in c.parameters) {
print("${p.type}" + " -- ")
}
println()
}
}
运行结果:
address -> Person
name -> Person
detailAddress -> Person,kotlin.String
isChild -> Person
equals -> kotlin.Any,kotlin.Any?
hashCode -> kotlin.Any
toString -> kotlin.Any
通过上面的运行结果我们发现,对于属性和无参数的函数,它们都有一个隐藏的参数为类的实例,而对于声明参数的函数,类的实例作为第 1 个参数,而声明的参数作为后续的参数。对于那些从Any
继承过来的参数,Kotlin 默认它们的第 1 个参数为Any
。
KType
每一个KCallabe
都可以使用returnType
来获取返回值类型,它的结果类型是一个KType
,代表着Kotlin中的类型。
API 描述 | 含义 |
---|---|
arguments: List<KTypeProjection> | 该类型的类型参数 |
classifier: KClassifier? | 得到结果为 List (忽略类型参数)的类型 |
isMarkedNullable: Boolean | 该类型是否标记为可空类型 |
classifier
API其实就是获取该参数在类层面对应的类型, 如 Int -> class kotlin.Int
, List<String> -> class kotlin.collections.List
。
KTypeParameter
在KClass
和KCallable
中我们可以通过typeParameters
来获取class
和callable
的类型参数,它返回的结果集是List<KTypeParameter>
,不存在类型参数时就返回一个空的List
。
fun <A> get(a: A) : A {
return a
}
然后我们可以使用下面的代码来获取get
方法和List<String>
的类型参数:
fun KTypeParameterShow() {
for (c in Person::class.members) {
if (c.name.equals("get")) {
println(c.typeParameters)
}
}
val list = listOf<String>("How")
println(list::class.typeParameters)
}
运行结果:
[A]
[E]
Kotlin 的注解
前面我们提及过注解 kotlin.Metadata
,这是实现 Kotlin 大部分独特特性反射的关键,Kotlin 将这些信息直接以注解形式存储在字节码文件中,以便运行时反射可以获取这些数据。
由于 Kotlin 兼容 Java,所以所有 Java 可以添加注解的地方,Kotlin 也都可以。并且 Kotlin 也简化了注解创建语法,创建注解就像创建 class
一样简单,只需额外在 class
前增加 annotation
关键字即可。
annotation class FooAnnotation(val bar: String)
上面的代码就直接创建了FooAnnotation
注解,和创建其他 Kotlin 的类一样,正如前文所说,只要在前面加上annotation
,这个类就变成了注解,和等价的 Java 代码相比较,确实简化了很多。
同时和 Java 一样,注解的参数只能是常量,并且仅支持下列类型:
- Java 对应的基本类型;
- 字符串;
- Class 对象(
KClass
或者Java的Class
); - 其他注解;
- 上述类型数组。注意基本类型数组需要指定为对应的
XXXArray
,例如IntArray
,⽽不是Array<Int>
。
元注解
类似@Target
这样标注在注解上的注解我们称之为元注解。我们知道 Java 中的java.lang.annotation
包中定义了下列 5 个元注解:
@Documented
文档(通常是API文档)中必须出现该注解。@Inherited
如果超类标注了该类型,那么其子类型也将自动标注该注解而无须指定。@Repeatable
这个注解在同一位置可以出现多次。@Retention
表示注解用途,有3种取值。Source
。仅在源代码中存在,编译后class
文件中不包含该注解信息。CLASS
。class
文件中存在该注解,但不能被反射读取。RUNTIME
。注解信息同样保存在class
文件中并且可以在运行时通过反射获取。
@Target
表明注解可应用于何处。
和 Java 一样在 Kotlin 中也有对应的元注解类。Kotlin 中的元注解类定义在 kotlin.annotation
包下,主要有:
Kotlin | Java | 含义 |
---|---|---|
@Retention | @Retention | 注解的保留期 |
@Target | @Target | 注解可用于哪些目标对象 |
@MustBeDocumented | @Documented | 注解将被文档工具提取到API文档中 |
@Repeatable | @Repeatable | 注解可以多次应用于相同的声明或类型 |
注意到,相比 Java 中5种元注解少了 @Inherited
,Kotlin 目前不支持 Inherited
,理论上实现继承没有很大难度,但当前版本还不支持。
通过上面对比我们发现,Kotlin 和 Java 注解整体上是保持一致的,熟悉 Java 注解的读者应该很容易将这部分知识迁移到 Kotlin。
@Target
@Target
顾名思义就是目标对象,也就是我们定义的注解能够应用于哪些目标对象,可以同时指定多个作用的目标对象。
@Target
的原型:
@Target(AnnotationTarget.ANNOTATION_CLASS)
@MustBeDocumented
public annotation class Target(vararg val allowedTargets : AnnotationTarget)
从@Target
的原型中我们可以看出,它接受一个vararg
可变数量的参数,所以可以同时指定多个作用的目标对象,并且参数类型限定为 AnnotationTarget
。
@Retention
@Retention
我们可以理解为保留期,和 Java 一样 Kotlin 有三种时期:源代码时期(SOURCE
)、编译时期(BINARY
)、运行时期(RUNTIME
)。
@Retention
的原型:
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class Retention(val value : AnnotationRetention = AnnotationRetention.RUNTIME)
Retention
接收一个AnnotationRetention
类型的参数,该参数有个默认值,默认是保留在运行时期。AnnotationRetention
是一个枚举类,其定义如下:
public enum class AnnotationRetention {
// Annotation isn't stored in binary output
SOURCE,
// Annotation is stored in binary output, but invisible for reflection
BINARY,
// Annotation is stored in binary output and visible for reflection (default retention)
RUNTIME
}
基本上对应了 Java 的三种类型,只不过 Kotlin 中默认值是 RUNTIME
。
AnnotationTarget
前面提到在@Target
元注解中可以同时指定一个或多个目标对象,那么到底有哪些目标对象呢?接下来让我们看一下:
Kotlin (Annotation Target) | Java (Target) | 说明 |
---|---|---|
CLASS | TYPE | 作用于类 |
ANNOTATION_CLASS | ANNOTATION_TYPE | 作用于注解本身(即元注解) |
TYPE_PARAMETER | TYPE_PARAMETER | 作用于类型参数 |
PROPERTY | N/A | 作用于属性 |
FIELD | FIELD | 作用于字段(属性通常包含字段 Getter 以及 Setter ) |
LOCAL_VARIABLE | FIELD | 作用于局部变量 |
VALUE_PARAMETER | N/A | 作用于 val 参数 |
CONSTRUCTOR | CONSTRUCTOR | 作用于构造函数 |
FUNCTION | METHOD | 作用于函数(Java只有Method ) |
PROPERTY_GETTER | N/A | 作用于 Getter |
PROPERTY_SETTER | N/A | 作用于 Setter |
TYPE | TYPE_USE | 作用于类型 |
EXPRESSION | N/A | 作用于表达式 |
FILE | PACKAGE | 作用于文件开头/包声明(两者有细微区别) |
TYPEALIAS | N/A | 作用于类型别名 |
Kotlin支持几乎所有Java支持的标注的位置,并且增加了一些kotlin独有的位置。
一个简单Kotlin注解使用的例子:
annotation class Cache(val namespace: String, val expires: Int)
annotation class CacheKey(val keyName: String, val buckets: IntArray)
@Cache(namespace = "hero", expires = 3600)
data class Hero(
@CacheKey(keyName = "heroName", buckets = intArrayOf(1,2,3))
val name: String,
val attack: Int,
val defense: Int,
val initHp: Int
)
Kotlin的代码常常会表达多重含义。例如,上述例子中的name
除了生成了一个不可变的字段之外,实际上还包含了Getter
,同时又是其构造函数的一个参数。
这就带来一个问题,@CacheKey
注解究竟是作用于何处?
精确控制注解的位置
为了解决这个问题,Kotlin 引入精确的注解控制语法,假如我们有注解 annotation class CacheKey
用法 | 含义 |
---|---|
@file:CacheKey | CacheKey 注解作用于文件 |
@property:CacheKey | CacheKey 注解作用于属性 |
@field:CacheKey | CacheKey 注解作用于字段 |
@get:CacheKey | CacheKey 注解作用于 Getter |
@set:CacheKey | CacheKey 注解作用于 Setter |
@receiver:CacheKey | CacheKey 注解作用于扩展函数或属性 |
@param:CacheKey | CacheKey 注解作用于构造函数参数 |
@setparam:CacheKey | CacheKey 注解作用 Setter 的参数 |
@delegate:CacheKey | CacheKey 注解作用于存储代理实例的字段 |
例如:
@Cache(namespace = "hero", expires = 3600)
data class Hero(
@property:CacheKey(keyName = "heroName", buckets = [1, 2])
val name: String,
@field:CacheKey(keyName = "atk", buckets = [1, 2, 3])
val attack: Int,
@get:CacheKey(keyName = "def", buckets = [1, 2, 3])
val defense: Int,
val initHp: Int
)
上述CacheKey
分别作用在属性、字段和Getter
上。
反射获取注解信息
这有一个前提就是这个注解的Retentaion
标注为Runtime
或者没有显示指定(注默认为Runtime
)。
annotation class Cache(val namespace: String, val expires: Int)
annotation class CacheKey(val keyName: String, val buckets: IntArray)
@Cache(namespace = "hero", expires = 3600)
data class Hero(
@CacheKey(keyName = "heroName", buckets = [1, 2, 3])
val name: String,
val attack: Int,
val defense: Int,
val initHp: Int
)
fun main() {
val cacheAnnotation = Hero::class.annotations.find{ it is Cache } as Cache?
println("namespace ${cacheAnnotation?.namespace}")
println("expires ${cacheAnnotation?.expires}")
}
通过反射获取注解信息是在运行时发生的,和Java一样存在一定的性能开销,当然这种开销大部分时候可以忽略不计。此外前面提到的注解标注位置也会影响注解信息的获取。例如@file:CacheKey
这样标注的注解,则无法通调用KProperty.annotions
获取到该注解信息。
注解的使用场景
- 提供信息给编译器:编译器可以利用注解来处理一些,比如一些警告信息,错误等
- 编译阶段时处理:利用注解信息来生成一些代码,在 Kotlin 生成代码非常常见,一些内置的注解为了与 Java API 的互操作性,往往借助注解在编译阶段生成一些额外的代码
- 运行时处理:某些注解可以在程序运行时,通过反射机制获取注解信息来处理一些程序逻辑
下面是一个通过注解来标注Http请求方法的代码示例:
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class HttpMethod(val method : Method)
interface Api {
val name: String
val version: String
get() = "1.0"
}
@HttpMethod(Method.POST)
class ApiGetArticles : Api {
override val name: String
get() = "/api.articles"
}
fun fire(api: Api) {
val annotations = api.javaClass.annotations
val method = annotations.find { it is HttpMethod } as? HttpMethod
println("通过注解得知该接口需需要通过:${method?.method}方式请求")
}
我们知道著名的网络请求库 Retrofit 就是通过这种方式来标注接口请求的方法、路径、参数等信息的。
加锁
虽然 Kotlin 是基于 Java 改良过来的语言,但是它没有 synchronized
关键字,取而代之,它使用了@Synchronized
注解和synchronized()
函数来实现等同的效果。比如:
class Shop {
val goods = hashMapOf<Long,Int>()
init {
goods.put(1,10)
goods.put(2,15)
}
@Synchronized
fun buyGoods(id: Long) {
val stock = goods.getValue(id)
goods.put(id, stock - 1)
}
fun buyGoods2(id: Long) {
synchronized(this) {
val stock = goods.getValue(id)
goods.put(id, stock - 1)
}
}
}
注意这里的synchronized(this)
是 kotlin 中的方法,而非 java 中的 synchronized
关键字。
Kotlin 除了支持 Java 中 synchronized
这种并发原语外,也同样支持其他一些并发工具,比如 volatile
关键字,java.util.concurrent.*
下面的并发工具。当然,Kotlin 也做了一些改造,比如 volatile
关键字在 Kotlin 中也变成了注解:
@Volatile private var running = false
除了可以用 synchronized
这种方式来对代码进行同步加锁以外,在 Java 中还可以用 Lock
的方式来对代码进行加锁。所以我们试着改造一下上面的 buyGoods
方法:
var lock: Lock = ReentrantLock()
fun buyGoods(id: Long) {
lock.lock()
try {
val stock = goods.getValue(id)
goods.put(id, stock - 1)
} catch (ex: Exception) {
println("[Exception] is ${ex}")
} finally {
lock.unlock()
}
}
但是这种写法似乎有如下不好之处:
- 若是在同一个类内有多个同步方法,将会竞争同一把锁;
- 在加锁之后,编码人员很容易忘记解锁操作;
- 重复的模板代码。
那么,我们现在试着对它进行改进,提高这个方式的抽象程度:
fun <T> withLock (lock: Lock, action: () -> T): T {
lock.lock()
try{
return action()
} catch (ex: Exception) {
println("[Exception] is ${ex}")
} finally {
lock.unlock()
}
}
withLock
方法支持传入一个lock
对象和一个Lamada
表达式,所以我们现在可以不用关心对buyGoods
进行加锁了,只需要在调用的时候传入一个lock
对象即可。
fun buyGoods(id: Long) {
val stock = goods.getValue(id)
goods.put(id, stock - 1)
}
var lock: Lock = ReentrantLock()
withLock(lock) {
buyGoods(1)
}
Kotlin 类库中也默认添加了该方式的支持:
var lock: Lock = ReentrantLock()
lock.withLock {
buyGoods(1)
}