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("執(zhí)行有參數的構造器:${name}")
}
//定義一個無參數的 info 方法
fun info() {
println("執(zhí)行無參數的 info 方法 ")
}
//定義一個有參數的 info 方法
fun info(str: String) {
println("執(zhí)行有參數的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 的子類)等對象渠旁,這些對象分別代表該類所包括的方法(包括構造器)和屬性等,程序還可以通過這些對象來執(zhí)行實際的功能船逮,例如調用方法顾腊、創(chuàng)建實例等。
創(chuàng)建對象
獲取 KClass 對象之后挖胃,調用該對象的 createlnstance()方法即可創(chuàng)建該類的實例杂靶,該方法總是調用 KClass所代表類的無參數的構造器來創(chuàng)建實例。
如果需要調用有參數的構造器來創(chuàng)建實例酱鸭,則可通過 KClass 的 constructors 屬性來獲取所有構造器吗垮,該屬性返回 Collection<Function>集合對象,這意味著構造器的本質依然是一個函 數凹髓。
獲取 KClass 的所有構造器之后烁登,接下來程序可根據需要調用指定的構造器來創(chuàng)建實例。下面程序示范了如何通過 KClass 創(chuàng)建實例 蔚舀。
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 ()方法調用無參數的構造器創(chuàng)建實例
val inst1 = clazz.createInstance()
//未知商品
//0.0
println(inst1.name)
println(inst1.price)
//獲取所有構造器
val cons = clazz.constructors
cons.forEach {
if (it.parameters.size == 2) {
//調用帶兩個參數的構造器創(chuàng)建實例
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("執(zhí)行帶 String 參數的 test 方法 : ${msg}")
}
fun test(msg: String, price: Double) {
println("執(zhí)行帶String, Double參數的 test方法: ${msg} ${price}")
}
}
fun main(args: Array<String>) {
val clazz = Foo1::class
//創(chuàng)建 Foo 類的實例
val ins = clazz.createInstance()
//獲取 clazz 所代表類直接定義的全部函數
val funs = clazz.declaredFunctions
for (f in funs) {
//如果函數具有 3 個參數(對應帶 2 個參數的方法)
if (f.parameters.size == 3) {
//調用帶 3 個參數的函數
//執(zhí)行帶String, Double參數的 test方法: Kotlin 78.8
f.call(ins, "Kotlin", 78.8)
}
//如果函數具有 2 個參數(對應帶 1 個參數的方法〉
if (f.parameters.size == 2) {
//調用帶 2 個參數的函數
//執(zhí)行帶 String 參數的 test 方法 : Kotlin
f.call(ins, "Kotlin")
}
}
}
從上面程序可以看出 鸥咖, 執(zhí)行帶3個參數的函數實際上是調用帶2個參數的方法,執(zhí)行帶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 類型 熬甫。
有些時候胰挑,程序需要實現某個功能較強的函數, 如果此時系統己經包含了多個細粒度的函數椿肩,那么可以將這些細粒度的函數組合起來實現功能較強的函數瞻颂。 比如業(yè)務需要程序獲取數的平方根,但該函數要做一些額外處理郑象,如果該數是正數贡这,則直接獲取平方根;如果該數是負數,則獲取該數的絕對值的平方根厂榛。
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: 代表無需調用者的屬性(靜態(tài)屬性)。它是 KProperty的子接口 忿危。
- KMutableProperty0: 代表無需調用者的讀寫屬性(靜態(tài)讀寫屬性)达箍。它是 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))
}
上面代碼獲取頂級讀寫屬性(靜態(tài)讀寫屬性)教届,程序直接用“::”加 屬性名的形式即可响鹃。該屬性引用是 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 支持一種“綁定的方法或屬性引用”靶草,這種方法或屬性引用不是通過類獲取的蹄胰,而是通過對象獲取的,這意味著該方法或屬性己經綁定了調用者奕翔,因此程序執(zhí)行這種方法或屬性時無須傳入調用者裕寨。
如下程序示范了綁定的方法或屬性引用。
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()方法即可獲取綁定的屬性值庆猫,無須傳入調用者。