Kotlin反射
正如在前面不少代码中所见到的, Kotlin也提供了反射 API,这些反射 API可以方便程序在运行时自省程序的结构 。 Kotlin把函数和属性当成“ 一等公民”, 并可通过反射直接获取函数、属性的引用。
使用 Kotlin的反射API需要添加单独的JAR文件(kotlin-reflect.jar),这样可以方使程序在不使用反射时减小运行库的大小。
类引用
Kotlin的类引用使用 KClass代表,如果要获取己知的 Kotlin类的 KClass对象,则可通过如下语法 :
val c = MyClass : :class
上面这种语法在前面程序中已经多次见到。需要说明的是,Kotlin的类引用是 KClass 对象, Java 的类引用是 java.lang.Class 对象,它们二者是不同的,如果需要通过KClass 获取对应的 java.lang.Class 对象,则可调用 KClass 对象的 java 属性 。
如果己有一个Kotlin对象, 则同样可通过::class语法来获取该对象的类引用。例如如下语法:
val c = myObj : :class
从KClass获取类信息
获取 KClass对象之后,即可通过KClass提供的大量方法或属性来获取该 KClass对象所对应类的详细信息。
下面程序示范了通过 KClass 来获取类的详细信息。该程序示范了 KClass 所包含的部分属性和方法
import kotlin.reflect.full.*
//定义注解
annotation class Anno
//使用 3 个注解修饰该类
@Deprecated("该类已经不推荐使用 ")
@Anno
@Suppress(" UNCHECKED CAST")
class ClassTest(age: Int) {
var name: String = "Kotlin"
//为该类定义一个私有的构造器
private constructor() : this(20) {
}
//定义一个有参数的构造器
constructor(name: String) : this(15) {
println("执行有参数的构造器:${name}")
}
//定义一个无参数的 info 方法
fun info() {
println("执行无参数的 info 方法 ")
}
//定义一个有参数的 info 方法
fun info(str: String) {
println("执行有参数的info方法,其str参数值:$str")
}
//定义一个测试用的嵌套类
class Inner
}
//为 ClassTest 定义扩展方法
fun ClassTest.bar() {
println("扩展的 bar 方法")
}
//为 ClassTest 定义扩展属性
val ClassTest.foo: Double
get() = 2.4
fun main(args: Array<String>) {
//下面代码可以获取 ClassTest 对应的 KClass
val clazz = ClassTest::class
//通过 constructors 属性获取 KClass 对象所对应类的全部构造器
val ctors = clazz.constructors
println("ClassTest 的全部构造器如下: ")
ctors.forEach {
println(it)
}
println("ClassTest 的主构造器如下: ")
println(clazz.primaryConstructor)
//通过 functions 属性获取该 KClass 对象所对应类的全部方法
var funs = clazz.functions
println("ClassTest 的全部方法如下: ")
funs.forEach {
println(it)
}
//通过 declaredFunctions 属性获取该 KClass 对象 本身所声明的全部方法(不包括继承的方法)
val funs2 = clazz.declaredFunctions
println("ClassTest 本身声明的全部方法如下: ")
funs2.forEach {
println(it)
}
//通过 declaredMemberFunctions 属性获取该 KClass 对象 本身所声明的全部成员方法(不包括继承的方法〉
val memberFunctions = clazz.declaredMemberFunctions
println("ClassTest 本身声明的成员方法如下: ")
memberFunctions.forEach {
println(it)
}
//通过 memberExtensionFunctions 属性获取该 KClass 对象 所代表类的全部扩展方法(不包括继承的方法)
val extensionFunctions = clazz.memberExtensionFunctions
println("ClassTest 本身声明的扩展方法如下: ")
extensionFunctions.forEach {
println(it)
}
//通过 declaredMemberProperties 属性获取该 KClass 对象 本身所声明的全部成员属性(不包括继承的属性〉
val memberProperties = clazz.declaredMemberProperties
println("ClassTest 本身声明的成员属性如下: ")
memberProperties.forEach {
println(it)
}
//通过 memberExtensionProperties 属性获取该 KClass 对象 所代表类的全部扩展属性(不包括继承的属性〉
val extensionProperties = clazz.memberExtensionProperties
println("ClassTest 本身声明的扩展属性如下: ")
extensionProperties.forEach {
println(it)
}
//通过 annotations 属性获取该 KClass 对象所对应类的全部注解
val anns = clazz.annotations
println("ClassTest 的全部注解如下: ")
anns.forEach {
println(it)
}
println("该 KClass 元素上的@Anno注解为: "+ clazz.findAnnotation<Anno>())
//通过 nestedClasses 属性获取该 KClass 对象所对应类的全部嵌套类(包括内部类和嵌套类)
val inners = clazz.nestedClasses
println("ClassTest 的全部内部类如下: ")
inners.forEach {
println(it)
}
//通过 supertypes 属性获取该类的所有父类型(包括父类和父接口)
println("ClassTest 的父类型为: " + clazz.supertypes)
}
运行结果:
ClassTest 的全部构造器如下:
fun <init>(): test11.ClassTest
fun <init>(kotlin.String): test11.ClassTest
fun <init>(kotlin.Int): test11.ClassTest
ClassTest 的主构造器如下:
fun <init>(kotlin.Int): test11.ClassTest
ClassTest 的全部方法如下:
fun test11.ClassTest.info(): kotlin.Unit
fun test11.ClassTest.info(kotlin.String): kotlin.Unit
fun kotlin.Any.equals(kotlin.Any?): kotlin.Boolean
fun kotlin.Any.hashCode(): kotlin.Int
fun kotlin.Any.toString(): kotlin.String
ClassTest 本身声明的全部方法如下:
fun test11.ClassTest.info(): kotlin.Unit
fun test11.ClassTest.info(kotlin.String): kotlin.Unit
ClassTest 本身声明的成员方法如下:
fun test11.ClassTest.info(): kotlin.Unit
fun test11.ClassTest.info(kotlin.String): kotlin.Unit
ClassTest 本身声明的扩展方法如下:
ClassTest 本身声明的成员属性如下:
var test11.ClassTest.name: kotlin.String
ClassTest 本身声明的扩展属性如下:
ClassTest 的全部注解如下:
@kotlin.Deprecated(level=WARNING, replaceWith=@kotlin.ReplaceWith(imports={}, expression=""), message="该类已经不推荐使用 ")
@test11.Anno()
该 KClass 元素上的@Anno注解为: @test11.Anno()
ClassTest 的全部内部类如下:
class test11.ClassTest$Inner
ClassTest 的父类型为: [kotlin.Any]
值得指出的是,虽然定义ClassTest类时使用了@Suppress 注解,但程序运行时无法分析出该类中包含的该注解,这是因为@Suppress 使用了@Retention(SOURCE)修饰,这表明 @Suppress 只能保存在源代码级别上,而通过 ClassTest.class 获取的是该类的运行时 KClass 对象,所以程序无法访问到@ Suppress 注解。
通过 KClass对象可以得到大量的 KFunction、 KProperty (它们都是 KCallable 的子类)等对象,这些对象分别代表该类所包括的方法(包括构造器)和属性等,程序还可以通过这些对象来执行实际的功能,例如调用方法、创建实例等。
创建对象
获取 KClass 对象之后,调用该对象的 createlnstance()方法即可创建该类的实例,该方法总是调用 KClass所代表类的无参数的构造器来创建实例。
如果需要调用有参数的构造器来创建实例,则可通过 KClass 的 constructors 属性来获取所有构造器,该属性返回 Collection<Function>集合对象,这意味着构造器的本质依然是一个函 数。
获取 KClass 的所有构造器之后,接下来程序可根据需要调用指定的构造器来创建实例。下面程序示范了如何通过 KClass 创建实例 。
import kotlin.reflect.full.*
class Item(var name: String) {
var price = 0.0
constructor() : this("未知商品") {
this.price = 0.0
}
constructor(name: String, price: Double) : this(name) {
this.price = price
}
}
fun main(args: Array<String>) {
val clazz = Item::class
//createinstance ()方法调用无参数的构造器创建实例
val inst1 = clazz.createInstance()
//未知商品
//0.0
println(inst1.name)
println(inst1.price)
//获取所有构造器
val cons = clazz.constructors
cons.forEach {
if (it.parameters.size == 2) {
//调用带两个参数的构造器创建实例
val inst2 = it.call("kotlin",78.9)
//kotlin
//78.9
println(inst2.name)
println(inst2.price)
}
}
}
构造器引用
正如前文所介绍的,构造器的本质是一个函数,即一个返回值为当前类实例的函数。 因此程序可将构造器引用当成函数使用。
此外, Kotlin 允许通过使用“::”操作符并添加类名来引用该类的主构造器 。 例如如下程序。
class Foo(var name: String = "未知")
//test 函数的参数是(String)->Foo 类型(这就是 Foo 带 String 参数的构造器的类型)
fun test(factory: (String) -> Foo) {
val x: Foo = factory("疯狂 Kotlin 讲义")
println(x.name)
}
fun main(args: Array<String>) {
//通过: :Foo 引用 Foo 类的主构造器
test(::Foo)
}
上面代码调用 test()函数时需要传入一个(String)->Foo 类型的参数,这就是 Foo 类主构造器的类型,因此将::Foo 作为参数传入 。
在某些时候,如果要获取 Kotlin构造器引用对应的 Java构造器对象(Constructor),则可通过调用 KFunction 的扩展属性 javaConstructor来实现。
例如如下代码:
::Foo. javaConstructor
需要说明的是,如果要调用构造器引用的 javaConstructor属性,则需要导入kotlin.reflect.jvm包,因为这些扩展属性都属于与 Java反射互相操作的部分,被定义在 kotlin.reflect.jvm包下。
调用方法
正如前面所见到的,所有构造器和方法都属于KFunction的实例,因此它们都可以通过call() 方法来调用。 所以,程序要调用指定类的方法,只要先获取方法的KFunction实例,然后调用call()方法即可 。
使用 KFunction调用方法时,有一点需要说明: 由于方法是面向对象的概念,因此它有一 个主调者。比如 一句汉语:“猪八戒吃西瓜”,换成面向对象的写法就是 :
猪八戒 .吃(西瓜)
但如果换成函数式写法就是 :
吃(猪八戒,西瓜)
对比这两种写法可以看出,面向对象的方法如果带 N个参数,那么转换成函数式调用时就会变成 N+I 个参数。
如下程序示范了调用指定函数。
class Foo1 {
fun test(msg: String) {
println("执行带 String 参数的 test 方法 : ${msg}")
}
fun test(msg: String, price: Double) {
println("执行带String, Double参数的 test方法: ${msg} ${price}")
}
}
fun main(args: Array<String>) {
val clazz = Foo1::class
//创建 Foo 类的实例
val ins = clazz.createInstance()
//获取 clazz 所代表类直接定义的全部函数
val funs = clazz.declaredFunctions
for (f in funs) {
//如果函数具有 3 个参数(对应带 2 个参数的方法)
if (f.parameters.size == 3) {
//调用带 3 个参数的函数
//执行带String, Double参数的 test方法: Kotlin 78.8
f.call(ins, "Kotlin", 78.8)
}
//如果函数具有 2 个参数(对应带 1 个参数的方法〉
if (f.parameters.size == 2) {
//调用带 2 个参数的函数
//执行带 String 参数的 test 方法 : Kotlin
f.call(ins, "Kotlin")
}
}
}
从上面程序可以看出 , 执行带3个参数的函数实际上是调用带2个参数的方法,执行带2个参数的函数实际上是调用带1个参数的方法,方法的调用者将作为函数的第一个参数被传入。
函数引用
Kotlin 的函数也是一等公民,函数也有其自身的类型。Kotiin 程序可以获取函数的引用,把函数当成参数传入另一个函数中。
Kotiin 也通过“::”符号加函数名的形式来获取特定函数的引用。当存在多个重载函数时, Kotlin可通过上下文推断出实际引用的是哪个函数: 如果 Kotiin无法通过上下文准确推断出引用哪个数,编译器就会报错。例如如下程序。
//定义两个重载的函数
fun isSmall(i: Int) = i < 5
fun isSmall(s: String) = s.length < 5
fun main(args: Array<String>) {
val list = listOf(20, 30, 100, 4, -3, 2, -12)
//由于 filter ()函数需要(Int) ->Boolean 类型的参数,故此处 ::isSmall 引用第一个函数
val resultList = list.filter(::isSmall)
println(resultList) //输出[ 4, - 3, 2, -12]
val strlist = listOf("Java", "Kotlin", "Swift", "Go", "Erlang")
//由于 filter () 函数需要(String)->Boolean 类型的参数, 故此处 ::isSmall 引用第二个函数
val resultStrList = strlist.filter(::isSmall)
println(resultStrList) //输出[ Java , Go]
//无法推断出 :: isSmall 到底引用哪个函数,报错
//val f = ::isSmall
//可以推断出 :: isSmall 到底引用哪个函数,正确
var f: (String) -> Boolean = ::isSmall
println(f("Lua"))
}
从代码所看到的 , 当 List 集合是 List<String>实例时,它的 filter()方法需要的参数是(String)->Boolean类型,因此 Kotlin可以准确地从重载函数中引用到符合要求的函数 。
此外需要说明的是,如果 需要引用类的成员方法或扩展方法,那么需要进行限定。例如 String::toCharArray才能表明引用 String的 toCharArray()方法,单纯地使用 ::toCharAηay()不行 。
String::toCharArray 函数引用的类型也不是简单的()->CharArray 类型,而是 String.() -> CharArray 类型 。
有些时候,程序需要实现某个功能较强的函数, 如果此时系统己经包含了多个细粒度的函数,那么可以将这些细粒度的函数组合起来实现功能较强的函数。 比如业务需要程序获取数的平方根,但该函数要做一些额外处理,如果该数是正数,则直接获取平方根;如果该数是负数,则获取该数的绝对值的平方根。
fun abs(d: Double): Double = if (d < 0) -d else d
fun sqrt(d: Double): Double = java.lang.Math.sqrt(d)
//定义一个 comp ()函数,该函数用于将两个函数组合起来
fun comp(fun1: (Double) -> Double, fun2: (Double) -> Double): (Double) -> Double {
return { x -> fun2(fun1(x)) }
}
fun main(args: Array<String>) {
println(abs(-3.2))
//将: :abs和::sqrt组合起来
val f = comp(::abs, ::sqrt)
println(f(-25.0))
}
上面代码用 comp()函数将::abs和::sqrt两个函数组合在一起,这样就会得到 一个新的函数 : f, 接下来程序可通过f()函数同时完成两个函数的功能。
在某些时候,如果要获取 Kotlin 函数引用对应的 Java 方法对象( Method),则可通过调用 KFunction 的扩展属性 javaMethod 来实现 。例如如下代码:
:: abs.javaMethod
需要说明的是,如果要调用函数引用的 javaMethod 属性 ,则需要导入kotlin.reflect.jvm包,因为这些扩展属性都属于与Java 反射互相操作的部分,被定义在 kotlin.reflect.jvm包下。
访问属性值
获取 KClass 对象之后,也可通过 KClass对象来获取该类所包含的属性。 Kotlin为属性提供了众多的 API。
- KProperty: 代表通用的属性 。 它是 KCallable 的子接口。
- KMutableProperty: 代表通用的读写属性。它是 KProperty的子接口。
- KProperty0: 代表无需调用者的属性(静态属性)。它是 KProperty的子接口 。
- KMutableProperty0: 代表无需调用者的读写属性(静态读写属性)。它是 KProperty0的子接口 。
- KProperty1 : 代表需要 1 个调用者的属性(成员属性)。它是 KProperty的子接口。
- KMutableProperty1:代表需要 1个调用者的读写属性(成员读写属性)。它是 KProperty1的子接口。
- KProperty2:代表需要 2 个调用者的属性(扩展属性) 。 它是 KProperty的子接口。
- KMutableProperty2:代表需要 2个调用者的读写属性(扩展读写属性)。它是 KProperty2的子接口。
程序获取代表属性的 KProperty对象之后,可调用 get()方法来获取属性的值;如果程序要设置属性的值,则需要获取代表属性的 KMutableProperty 对象。
如下程序示范了如何通过反射来设置和获取属性的值 。
class Item2 {
var name: String = "kotlin"
val price: Double = 24.1
}
fun main(args: Array<String>) {
val clazz = Item2::class
val ins = clazz.createInstance()
val props = clazz.declaredMemberProperties
props.forEach {
when (it.name) {
"name" -> {
@Suppress("UNCHECKED_CAST")
//将属性转换为读写属性
val mp = it as KMutableProperty1<Item2, Any> //修改属性值
mp.set(ins, "Java")
println(it.get(ins))
}
"price " -> {
//只读属性,只能通过 get ()方法读取属性值
println(it.get(ins))
}
}
}
}
正如上面代码所看到的,当程序要设置 name 属性的属性值时,由于 name 属性是一个成员读写属性,因此程序将该属性转型为 KMutableProperty1 对象,转型之后程序可调 用 set()方法来设置该属性的值,如上面代码所示:如果程序只需获取该属性的值,则调用 KProperty1 的 get()方法即可,如上面代码所示。
属性引用
Kotiin 同样提供了“::”符号加属性名的形式来获取属性引用,获取属性引用也属于前面介绍的Kproperty 及其子接口的实例 。
获取 Kotiin 只读属性的引用之后,程序可调用 get()方法来获取属性的值;获取 Kotiin读写属性的引用之后,程序可调用 set()方法来修改属性的值,也可调用 get()方法来获取属性的值。如下程序示范了通过属性引用来操作属性。
class Item3 {
var name: String = "kotlin"
var price = 16.1
}
var foo = "foo"
fun main(args: Array<String>) {
//获取 foo 属性,属于 KMutablePropertyO 的实例
val topProp = ::foo
topProp.set("修改后的属性")
// println(topProp.get())
println(foo)
val im = Item3()
//获取 Item 的 name 属性,属于 KMutablePropertyl1的实例
var namePro = Item3::name
namePro.set(im, "xq")
println(namePro.get(im))
//获取 Item 的 price 属性,属于 KProperty1 的实例
val prop = Item3::price
println(prop.get(im))
}
上面代码获取顶级读写属性(静态读写属性),程序直接用“::”加 属性名的形式即可。该属性引用是 KMutableProperty0 的实例 , 因此该属性既可通过 set()方法 改变属性的值,也可通过 get()方法获取属性的值。
第代码获取 Item类的name读写属性,程序需要用类名::属性名的形式来获取指定类的读写属性。该属性是 KMutableProperty1 的实例,因此程序也可通过 set()、 get()方法来修改、获取属性的值 。
与前面介绍的构造器、函数相似, Kotiin 在 kotlin.reflect.jvm 包下也提供了Kotlin 属性与Java 反射互操作的扩展属性 。 由于 Kotlin 属性会对应于 Java 的 3 种成员,因此 KProperty 包含如下 3 个扩展属性 。
- javaField : 获取该属性的幕后字段(如果该属性有幕后字段的话)。 该属性返回java.lang.reflect.Field对象。
- javaGetter : 获取该属性的 getter方法 。 该属性返回 java.lang.reflect.Method 对象 。
- javaSetter:获取该属性的 setter 方法(如果该属性是读写属性的话) 。 该属性返回
java.lang.reflect. Method对象。
一旦获取了 Kotlin属性的幕后字段的 Field对象、getter和 setter方法的Method对象之后,剩下的就是 Java反射的事了。如下程序示范了通过 Kotiin属性来获取 Java反射 API。
class Item3 {
var name: String = "kotlin"
var price = 16.1
}
var foo = "foo"
fun main(args: Array<String>) {
//获取 foo 属性,属于 KMutablePropertyO 的实例
val topProp = ::foo
println(topProp.javaField)
//获取幕后字段
println(topProp.javaGetter)
//获取 getter方法
println(topProp.javaSetter)
//获取 Item3 的 name 属性,属于 KMutableProperty1 的实例
val mp = Item3::name
//获取幕后字段
println(mp.javaField)
//获取 getter 方法
println(mp.javaGetter)
//获取 setter方法
println(mp.javaSetter)
//获取 Item3 的 price 属性 ,属于 KMutableProperty1 的实例
val prop = Item3::price
//获取幕后字段
println(prop.javaField)
//获取 getter 方法
println(prop.javaGetter)
}
绑定的方法与属性引用
前面介绍的都是通过 KClass (类本身)来获取方法或属性的引用的,当函数或属性不在任何类中定义时,程序直接使用“::”加函数名(或属性名)的形式来获取函数或属性的引用, 这些函数或属性都没有绑定任何对象,因此调用函数或属性时第一个参数必须传入调用者。
从 Kotlin 1.1 开始, Kotlin 支持一种“绑定的方法或属性引用”,这种方法或属性引用不是通过类获取的,而是通过对象获取的,这意味着该方法或属性己经绑定了调用者,因此程序执行这种方法或属性时无须传入调用者。
如下程序示范了绑定的方法或属性引用。
fun main(args: Array<String>) {
val str = "Kotlin"
//获取对象绑定的方法
val f: (CharSequence, Boolean) -> Boolean = str::endsWith
//调用绑定的方法时无须传入调用者
println(f("lin", true))
//获取对象绑定的属性
val prop = str::length
//调用绑定的属性时无须传入调用者
println(prop.get())
var list = listOf("Kotlin", "Java", "Go", "Erlang")
//获取对象绑定的方法
val fn = list::subList
//调用绑定的方法时无须传入调用者
println(fn(1, 3)) //输出["Java","Go"]
// 获取对象绑定的属性
val prp = list::indices
//调用绑定的属性时无须传入调用者
println(prp .get()) //输出 0 .. 3
}
上面程序先定义了 一个简单的字符串,代码使用对象来获取绑定的方法, 这样当程序调用方法时只要传入该方法的两个参数即可,无须传入调用者;后面代码使用对象来获取绑定的属性,这样程序直接使用 get()方法即可获取绑定的属性值,无须传入调用者。