美文网首页
Kotlin和Java互相调用(二)

Kotlin和Java互相调用(二)

作者: 凌寒天下独自舞 | 来源:发表于2018-11-08 10:57 被阅读0次

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()方法即可获取绑定的属性值,无须传入调用者。

相关文章

网友评论

      本文标题:Kotlin和Java互相调用(二)

      本文链接:https://www.haomeiwen.com/subject/czwvxqtx.html