《Kotlin核心编程》笔记:反射、注解和加锁

Kotlin 和 Java 反射


  • 1)Kotlin 的 KClass 和 Java 的 Class 可以看作同一个含义的类型,并且可以通过.java.kotlin方法在KClassClass之间互相转化。
  • 2)Kotlin 的 KCallable 和 Java 的 AccessiableObject 都可以理解为可调用元素。Java 中构造方法为一个独立的类型,而 Kotlin 则统一作为 KFunction 处理。
  • 3)Kotlin 的 KProperty 和 Java 的 Field 不太相同。Kotlin 的KProperty通常指相应的GetterSetter (只有可变属性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

KMutablePropertyKProperty的一个子类,那我们如何识别一个属性是KMutableProperty还是KProperty呢?参考如下代码:

kotlin 复制代码
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 该参数的类型
kotlin 复制代码
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()
    }
}

运行结果:

kotlin 复制代码
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.IntList<String> -> class kotlin.collections.List

KTypeParameter

KClassKCallable中我们可以通过typeParameters来获取classcallable的类型参数,它返回的结果集是List<KTypeParameter>,不存在类型参数时就返回一个空的List

kotlin 复制代码
fun <A> get(a: A) : A { 
	return a
}

然后我们可以使用下面的代码来获取get方法和List<String>的类型参数:

kotlin 复制代码
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)
}

运行结果:

kotlin 复制代码
[A]
[E]

Kotlin 的注解

前面我们提及过注解 kotlin.Metadata,这是实现 Kotlin 大部分独特特性反射的关键,Kotlin 将这些信息直接以注解形式存储在字节码文件中,以便运行时反射可以获取这些数据。

由于 Kotlin 兼容 Java,所以所有 Java 可以添加注解的地方,Kotlin 也都可以。并且 Kotlin 也简化了注解创建语法,创建注解就像创建 class 一样简单,只需额外在 class 前增加 annotation 关键字即可。

kotlin 复制代码
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文件中不包含该注解信息。
    • CLASSclass文件中存在该注解,但不能被反射读取。
    • 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的原型:

kotlin 复制代码
@Target(AnnotationTarget.ANNOTATION_CLASS)
@MustBeDocumented
public annotation class Target(vararg val allowedTargets : AnnotationTarget) 

@Target的原型中我们可以看出,它接受一个vararg可变数量的参数,所以可以同时指定多个作用的目标对象,并且参数类型限定为 AnnotationTarget

@Retention

@Retention 我们可以理解为保留期,和 Java 一样 Kotlin 有三种时期:源代码时期(SOURCE)、编译时期(BINARY)、运行时期(RUNTIME)。

@Retention的原型:

kotlin 复制代码
@Target(AnnotationTarget.ANNOTATION_CLASS)
public annotation class Retention(val value : AnnotationRetention = AnnotationRetention.RUNTIME) 

Retention 接收一个AnnotationRetention类型的参数,该参数有个默认值,默认是保留在运行时期AnnotationRetention 是一个枚举类,其定义如下:

kotlin 复制代码
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注解使用的例子:

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 注解作用于存储代理实例的字段

例如:

kotlin 复制代码
@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)。

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 = [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请求方法的代码示例:

kotlin 复制代码
@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()函数来实现等同的效果。比如:

kotlin 复制代码
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 中也变成了注解:

kotlin 复制代码
@Volatile private var running = false

除了可以用 synchronized 这种方式来对代码进行同步加锁以外,在 Java 中还可以用 Lock 的方式来对代码进行加锁。所以我们试着改造一下上面的 buyGoods 方法:

kotlin 复制代码
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()
    }
}

但是这种写法似乎有如下不好之处:

  • 若是在同一个类内有多个同步方法,将会竞争同一把锁;
  • 在加锁之后,编码人员很容易忘记解锁操作;
  • 重复的模板代码。

那么,我们现在试着对它进行改进,提高这个方式的抽象程度:

kotlin 复制代码
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对象即可。

kotlin 复制代码
fun buyGoods(id: Long) {
    val stock = goods.getValue(id)
    goods.put(id, stock - 1)
}

var lock: Lock = ReentrantLock()
withLock(lock) {
	buyGoods(1)
}

Kotlin 类库中也默认添加了该方式的支持:

kotlin 复制代码
var lock: Lock = ReentrantLock()
lock.withLock { 
	buyGoods(1)
}
相关推荐
xvch43 分钟前
Kotlin 2.1.0 入门教程(七)
android·kotlin
zhangphil3 小时前
Android BitmapShader简洁实现马赛克,Kotlin(一)
android·kotlin
五味香10 小时前
Java学习,查找List最大最小值
android·java·开发语言·python·学习·golang·kotlin
五味香1 天前
Java学习,List截取
android·java·开发语言·python·学习·golang·kotlin
xvch2 天前
Kotlin 2.1.0 入门教程(三)
android·kotlin
小李飞飞砖2 天前
kotlin的协程的基础概念
开发语言·前端·kotlin
深色風信子2 天前
Kotlin Bytedeco OpenCV 图像图像49 仿射变换 图像裁剪
opencv·kotlin·javacpp·bytedeco·仿射变换 图像裁剪
五味香3 天前
Java学习,List移动元素
android·java·开发语言·python·学习·kotlin·list
studyForMokey3 天前
【Android学习】Kotlin随笔
android·学习·kotlin
zhangphil4 天前
Android BitmapShader实现狙击瞄具十字交叉线准星,Kotlin
android·kotlin