kotlin与java互相调用

Kotlin 调用 Java

属性

Kotlin 调用属性实际上就是访问 getter、setter 方法,因此 Java 类只要提供了 getter 方法,Kotlin 就可将其当成只读属性;如果 Java 类提供了 getter、setter 方法, Kotlin 就可将其当成读写属性,Java 是否包含成员变量不重要,关键是 getter、setter 方法

如果 getter 方法的返回值类型是 boolean,Kotlin 会将其当成属性名与 getter 方法同名的属性

public class User {
    private String name;

    // name 的 setter、getter 方法
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    //当Kotlin调用时,会其当成名为 age 的只读属性
    public int getAge() {
        Log.d("TAG", "getAge: ");
        return 9;
    }

    // married 的 setter、getter 方法
    public boolean isMarried() {
        Log.d("TAG", "isMarried: ");
        return true;
    }

    public void setMarried(boolean married) {
        Log.d("TAG", "setMarried: ");
    }
}
//操作 name 读写属性
user.name = "kotlin"
Log.d(TAG, "onCreate: name=${user.name}")
//操作 isMarried 读写属性
user.isMarried = true
Log.d(TAG, "onCreate: name=${user.isMarried}")
//读取 age 只读属性
Log.d(TAG, "onCreate: name=${user.age}")

void 和调用名为关键字的成员

Java 方法的返回值是 void ,Kotlin 中则对应于 Unit 返回值类型

由于 Kotlin 的关键字比 Java 多(比如 Kotlin 的 is、 object、in 在 Java 语言中都不是关键字) ,因此可能出现一种情况: Java 的类名、接口名、方法名等是 Kotlin 的关键字。此时就需要使用反引号(就是键盘上数字 1 左边的那个键)对关键宇进行转义

public class User {
    public void in () {
        Log.d("TAG", "in: ");
    }
}
val user = User()
user.`in`() // 对 in 关键字转义

Katlin 的已映射类型

虽然 Kotlin 并未完整地提供整套类库,但 Kotlin 还是为部分 Java 类提供了特殊处理,这部分 Java 类被映射到 Kotlin 类,这种映射只在编译阶段发生,在运行阶段依然使用 Java 类型

Java 基本类型与 Kotlin 类之间的映射关系

Java 基本类型 Kotlin类
byte kotlin.Byte
short kotlin.Short
int kotlin.Int
long kotlin.Long
char kotlin.Char
float kotlin.Float
double kotlin.Double
boolean kotlin.Boolean

Java 基本类型的包装类则映射到 Kotlin 类的可空类型

Java 基本类型的包装类 Kotlin类
java.lang.Byte kotlin.Byte?
java.lang.Short kotlin.Short?
java.lang.Int kotlin.Int?
java.lang.Long kotlin.Long?
java.lang.Char kotlin.Char?
java.lang.Float kotlin.Float?
java.lang.Double kotlin.Double?
java.lang.Boolean kotlin.Boolean?

Java 的常用类与 Kotlin 类之间的映射关系

Java 常用类 Kotlin类
java.lang.Object kotlin.Any
java.lang.Cloneable kotlin.Cloneable
java.lang.Comparable kotlin.Comparable
java.lang.Enum kotlin.Enum
java.lang.Annotation kotlin.Annotation
java.lang.Deprected kotlin.Deprected
java.lang.CharSequence kotlin.CharSequence
java.lang.String kotlin.String
java.lang.Number kotlin.Number
java.lang.Throwable kotlin.Throwable

Kotlin 对 Java 泛型的转换

Kotlin 不支持 Java 的泛型通配符语法,但 Kotlin 提供了使用处型变来代替泛型通配符。因此 Java 的通配符将会转换为 Kotlin 的使用处型变(类型投影)。此外, Java 的原始类型则转换为 Kotlin 的星号投影

Java 泛型和 Katlin 泛型的转换关系

Java 泛型 Kotlin 语法
Foo<? extends Bar> Foo<out Bar!>!
Foo<? super Bar> Foo<in Bar!>!
Foo<*> Foo<out Any?>!
Foo Foo<*>

Kotlin 和 Java 一样,它们都无法在运行时保留泛型信息,因此在 Kotlin 使用 is 运算符进行类型检测时只能检测星号投影(相当于 Java 的原始类型),不能检测泛型信息

val list = listOf (2, 3 , 10)
// is 判断不能检测泛型参数,下面语句会编译报错
//Log.d(TAG, "onCreate: ${list is List<String>}")
// is 只能检测星号投影,输出 true
Log.d(TAG, "onCreate: ${list is List<*>}")

Java 数组的处理

相比 Java 数组, Kotlin 数组有个重大的改变, Kotlin 数组是不型变的。因此 Array<String>不能赋值给 Array<Object> 变量。 Java 数组不同, Java 数组是型变的,因此 String[] 可以接赋值给 Object[],Java 数组的型变可以说是一个缺陷,如下代码编译完全没有问题,但运行时就会引发 ArrayStoreException 异常

String[] strArr = new String[10];
Object[] objArr = strArr;
objArr[0] = 2;

Java 还支持 int[]、 long[] 等基本类型的数组,这种数组可以避免拆箱、装箱带来的性能开销,对应于 Kotlin 的 IntArray、 LongArray 数组

public void test(int[] arr) {
    Log.d("TAG", "test: ");
}

对于 Java 的 test 方法,它需要的参数是 int 类型,因此 Kotlin 程序不能用 Array<Int> 数组来调用该方法,而应该使用 IntArray 数组来调用该方法

val user = User()
//由于 sum() 方法需要的参数是 int[] 类型,因此此处需要使用 IntArray 对象
val intArr = intArrayOf(2, 4, 10)
Log.d(TAG, "onCreate: ${user.test(intArr)}")

调用参数个数可变的方法

对于参数个数可变的方法, Java 可以直接传入一个数组,但 Kotlin 不行。 Kotlin 要求只能传入多个参数值,但也可通过使用“*”解开数组的方式来传入多个数组元素作为参数值

//定义参数个数可变的方法
public void test(int... nums) {
    Log.d("TAG", "test: ");
}
val user = User()
//直接传入多个 int 值来调用
Log.d(TAG, "onCreate: ${user.test(2, 5, 8)}")
//将数组解开成多个元素
val intArr = intArrayOf(2, 4, 10)
Log.d(TAG, "onCreate: ${user.test(*intArr)}")

checked 异常

由于 Kotlin 没有 checked 异常,因此对于 Java 中可能引发 checked 异常的方法、构造器, Kotlin 则不会引发该异常, Kotlin 既可捕获该异常,也可完全不理会该异常

//下面代码可能引发 IOException (checked 异常)
//但 Kotlin 并不强制处理该异常
val fis = FileInputStream("a.txt")
Log.d(TAG, "onCreate: ${fis.read()}")

Object 的处理

Java 的 java.lang.Object 对应于 Kotlin 中的 Any,又因为 Any 只声明了 toString()、 hashCode()、equals() 方法,因此需要考虑如何使用 java.lang Object 类中其他方法的问题

wait()/notify()/notifyAll()

这三个方法都是 Java 程序中同步监视器支持调用的方法, 一般来说,只有在线程通信中才会用到,而且 Java 提供了更好的 Condition 来控制线程通信,因此,一般不需要调用这三个方但如果编程时真的需要让同步监视器调用这三个方法,则可以在程序中将 Any 转型为 java.lang.Object ,然后再调用这三个方法

(foo as java.lang .Object).wait()

getClass()

java.lang.Object 对象的 getClass() 方法用于获取该对象的 Java 类, Kotlin 的对象则有两种方式来获取该对象的 Java 类

//获取 obj 对象的 Java
obj::class.java
obj.javaClass

clone()

如果要让对象重写 clone 方法, 需要让该类实现 kotlin.Cloneable 接口

class Example : Cloneable {
    override fun clone(): Any {
        //...... 
    }
}

finalize()

java.lang. Object 的 finalize() 方法主要用于完成一些资源清理的工作, GC 在回收某个对象之前, JVM 会自动调用该对象的 finalize() 方法, 如果要重写 finalize() 方法 则只要在类中实现该方法即可,不需要使用 override 关键字(因为在 Any 类中并没有声明 finalize() 方法),根据 Java 的规则, finalize() 方法不能定义成 private

class Example : Cloneable {
    protected fun finalize () {
        //实现资源清理的逻辑
    }
}

访问静态成员

虽然 Kotlin 本身没有提供 static 关键字,但 Kotlin 提供了伴生对象来实现静态成员,因此 Java 类中的静态成员都可通过伴生对象的语法来调用

//调用 Runtime 的静态方法,就像调用伴生对象的方法一样
val rt = Runtime.getRuntime()

SAM 转换

Java 8 支持使用 Lambda 表达式来作为函数式接口的实例(这种机制被称为 SAM 转换),Kotlin 同样支持这个功能

//使用 Lambda 表达式来创建函数式接口( Predicate )的对象
val pred = Predicate<Int> { t -> t > 5 }
val list = arrayListOf(2, 200, 10, 34, 30, 44)
//使用 pred 对 List 集合进行过滤
list.removeIf(pred)
Log.d(TAG, "onCreate: list=$list") //输出[2]
// 使用 Lambda 表达式来创建函数式接口( Runnable )的对象
val rn = Runnable {
    for (i in 0..10) {
        Log.d(TAG, "onCreate: i=$i")
    }
}
//通过 Runnable 对象创建、启动线程
Thread(rn).start()
val executor = Executors.newCachedThreadPool()
//由于 executor 的 execute() 方法需要 Runnable 对象
//因此程序可自动将符合该接口规范的 Lambda 表达式 建成 Runnable 对象
executor.execute {
    Log.d("TAG", "onCreate: This runs in a thread pool")
}

//当然也可在方法中显式指定 Lambda 表达式创建的是 Runnable 对象
executor.execute(Runnable {
    Log.d("TAG", "onCreate: This runs in a thread pool")
})

与 Java 类似, Lambda 表达式只能创建函数式接口的对象,不能创建抽象类的对象

Kotlin 中使用 JNI

如果要在 Java 中使用 JNI ,则应该使用 native 修饰该方法,而 Kotlin 使用 external 关键字

external fun foo(x: Int): Double

Java 调用 Kotlin

属性

Kotlin 属性可编译成如下三个成员

  1. 一个 private 实例变量,实例变 名与属性名相同(如果该属性具有幕后字段)
  2. 一个 getter 方法,方法名为属性名添加 get 前缀
  3. 一个 setter 方法,方法名为属性名添加 set 缀(读写属性才有 setter 方法)

但如果属性名以 is 开头(属性类型并不要求一定是 Boolean 类型), 那么该属性对应的生成规 略有差别:setter 方法名为属性名去掉 is 前缀,添加 set 前缀

例如如下属性:

var isMarried : Boolean

该属性将会被 Kotlin 编译成如下三个成员

private boolean isMarried;

public boolean isMarried() {
    return isMarried;
}

public void setMarried(boolean isMarried) {
    this.isMarried = isMarried;
}

包级函数

所谓包级函数,是指在 Kotlin 中直接定义的顶级函数(不在任何类中定义), 但实际上 Kotlin 编译器会为整个Kotlin 源文件生成一个类(只要该源文件中包含了顶级函数、顶级变量),而这些顶级函数、顶级变量都会变成该类的静态方法、静态变量

文件MainActivity:

class MainActivity : AppCompatActivity() {

    @RequiresApi(Build.VERSION_CODES.N)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

var name: String = ""
fun test() {

}

由于上面源文件中包含了顶级成员(顶级函数或变量) ,在默认情况下, Kotlin 编译器会为该源文件生成 MainActivityKt. class 文件,这些顶级成员就变成该 MainActivityKt 的静态成员

public class MainActivityKt {
    private static java.lang.String name;

    public static final java.lang.String getName() {
        //...
    }

    public static final void setName(java.lang.String) {
        //...
    }

    public static final void test() {
        //...
    }
}

在默认情况下, Kotlin 为包含顶级成员的源文件所生成类的类名总是文件名+Kt 后缀,Kotlin 也允许使用 @JvmName 注解(该注解用来修饰文件本身,它是 AnnotationRetention.SOURCE 的注解)来改变 Kotlin 编译生成的类名

//指定生成的类
@file:JvmName("Test")
package com.example.kotlin

java中调用

//调用 Test 的 test() 方法
Test.test();

还有一种极端情况,就是多个 Kotlin 源文件要生成相同的 Java 类(包名相同且类名相同,或指定了相同的@JvmName 注解),这种情况默认会引发错误。我们可以指定 Kotlin 为这些 Kotlin 源文件统一生成 Java 类,而该 Java 类将会包含不同源文件中的顶级成员。为了实现这种效果,需要使用@JvmMultifileClass 注解

//指定生成的类
@file:JvmName("Test")
//指定为多个文件中的顶级成员统一生成一个类
@file:JvmMultifileClass
package com.example.kotlin

实例变量

Kotlin 允许将属性暴露成实例变量,只要在程序中使用@JvmField 修饰该属性即可,暴露出来的属性将会和 Kotlin 属性具有相同访问权限

使用 @JvmField 将属性暴露成实例变量的要求如下:

  1. 该属性具有幕后字段
  2. 该属性必须是非 private 访问控制的
  3. 该属性不能使用 open、override、const 修饰
  4. 该属性不能是委托属性
class Test(name: String) {
    //这样系统就会把该属性的幕后字段暴露成实例变量
    @JvmField
    var myName = name
}

Java 访问该类的对象的实例变量

Test test = new Test("java");
test.myName = "kotlin";

类变量

在命名对象(对象声明)或伴生对象中声明的属性会在该命名对象或包含伴生对象的类中具有静态幕后字段(类变量)。但这些类变量通常是 private 访问权限,程序可通过如下几种方式之一将它们暴露出来

  1. 使用 @JvmField 注解修饰
  2. 使用 lateinit 修饰
  3. 使用 const 修饰
class Test {
    //使用 companion 修饰的伴生对象
    companion object {
        @JvmField
        var myName = "kotlin"
    }
}

伴生对象是用来弥补 Kotlin 没有 static 关键字的不足的,因此伴生对象中的属性实际上就相当于Test 的类变量,但它默认是 private 访问权限。上面粗体字代码使用了@JvmField 修饰,这样该类变量就变成了与该属性具有相同的访问控制符: public

Java中访问

Test.myName = "kotlin";

在命名对象或伴生对象中的延迟初始化属性 (lateinit )具有与该属性的 setter 方法相同的访问控制符的类变量

//定义命名对象
object MyObject {
    //定义延迟初始化属性
    lateinit var name: String
}
MyObject.name = "kotlin";

此外,在 Kotlin 程序中使用 const 修饰的属性,不管是在顶层定义的属性,还是在对象中定义的属性,只要使用了 const 修饰,它就会变成有 public static final 修饰的类变量

const val MAX = 239

object Obj {
    const val NAME = "kotlin"
}

class Test {
    //使用 companion 修饰的伴生对象
    companion object {
        //使用 const 修饰的变量
        const val myName = "kotlin"
    }
}

java中调用

Log.d("TAG", "test: "+ MainActivityKt.MAX);
Log.d("TAG", "test: "+ Obj.NAME);
Log.d("TAG", "test: "+ Test.myName);

类方法

Kotlin 的顶级函数会被转换成类方法,此外, Kotlin 还可以将命名对象或伴生对象中定义的方法转换成类方法一一如果这些方法使用 @JvmStatic 修饰的话

class Test {
    companion object {
        @JvmStatic
        fun test () {
            Log.d("TAG", "test: companion object")
        }
    }
}

//定义命名对象
object MyObject {
    @JvmStatic
    fun test () {
        Log.d("TAG", "test: MyObject")
    }
}

java调用

//访问 Test 类的类方法
Test.test();
//访问 MyObject 类的类方法
MyObject.test();
//通过 INSTANCE 访问 MyObject 的单例,通过单例访问 test() 方法
MyObject.INSTANCE.test();
//通过 Companion 访问 Test 的伴生对象,通过伴生对象访问 test() 方法
Test.Companion.test();

访问控制符的对应关系

Kotlin 的访问控制符 Java的访问控制符
private private
protected protected,但需要注意的是,在 Java 程序中,protected 成员可以被同一个包中的其他成员访问,但 Kotlin 不行,Kotlin 中的 protected只能被当前类及其子类成员访问
internal public,但编译成 Java 类时会通过名字修饰来避免在 Java 中被意外使用到
public public

获取 KClass

为了在 Java 中获取 Kotlin 的 KClass 对象,必须通过调用 Class<T> .kotlin扩展属性的等价形式来实现

Class<?> clazz = Class.forName("java.util.ArrayList");
Log.d("TAG", "test: clazz=" + clazz);
//获取 Class 对象的 KClass 对象
KClass kc = kotlin.jvm.JvmClassMappingKt.getKotlinClass(clazz);
Log.d("TAG", "test: kc=" + kc);

使用 @JvmName 解决签名冲突

有些时候,在 Kotlin 中要定义两个同名函数,但 JVM 平台无法区分这两个函数。典型的情况就是类型擦除引起的问题

fun List<String>.filterValid(): List<String> {
    val result = mutableListOf<String>()
    for (s in this) {
        if (s.length < 5) {
            result.add(s)
        }
    }
    return result.toList()
}

@JvmName("filterValidInt")
fun List<Int>.filterValid(): List<Int> {
    val result = mutableListOf<Int>()
    for (i in this) {
        if (i < 20) {
            result.add(i)
        }
    }
    return result.toList()
}

由于编译器编译之后会产生类型擦除,因此上面两个方法的签名都是List.filterValid():List,为了让JVM 平台能区分这两个方法,程序中粗体字代码使用了 @JvmName("filterValidInt")来标注后一个方法,因此后一个方法在 JVM 平台上会编译成 fileterValidlnt() 方法

@JvmName 注解也适用于属性 x 和函数 getX() 共存

val x: Int
    @JvmName("getX_prop")
    get() = 2

fun getX() = 9

生成重载

Kotlin 为方法(或函数)提供了参数默认值来避免函数重载过多的问题。但对于这种参数有默认值的方法(或函数),编译器默认只生成一个方法:默认带所有参数的方法,为了让编译器能为带参数默认值的方法(或函数)生成多个重载的方法(或函数),可考虑使用 @JvmOverloads 注解

@JvmOverload 注解也适用于构造器、静态方法等。它不适用于抽象方法,包括在接口中定义的方法

@JvmOverloads
fun test(name: String, age: Int = 20, gender: String = "男") {
    Log.d("TAG", "test: name=$name")
    Log.d("TAG", "test: age=$age")
    Log.d("TAG", "test: gender=$gender")
}

上面定义了一个带两个默认参数的函数,井使用 @JvmOverloads 修饰该函数,这样编译器将会为该函数生成如下三个方法

public static final void test(java.lang.String, int, double)

public static final void test(java.lang.String, int)

public static final void test(java.lang.String)

如果一个类的所有构造器的参数都有默认值,这样就能以构造参数的默认值来调用构造器(就像调用无参数的构造器),因此在 JVM 平台上相当于生成 public 的无参数构造器,这个功能不需要 @JvmOverloads 注解

checked 异常

Kotlin 没有 checked 异常,如果我们希望 Java 调用该函数时编译器会检查 checked 异常 ,则可使用 @Throws 注解修饰该函数

@Throws(IOException::class)
fun foo() {
    val fis = FileInputStream("a.txt")
}

泛型的型变

Java 泛型只支持使用处型变,因此,对于 Kotlin 声明处型变,必须转换成 Java 的使用处型变

  1. 对于协变类型的泛型类 Bar<out T>,当它作为参数出现时, Kotlin 会自动将 Bar<Base>类型的参数替换成 Bar<? extends Base>
  2. 对于逆变类型的泛型类 <Foo in>,当它作为参数出现时, Kotlin 会自动将 Foo<Sub> 类型的参数替换成 Foo<? super Sub>
  3. 不管是协变类型的泛型类 Bar<out T> ,还是逆变类型的泛型类 Foo<in T>,当它作为返回值出现时, Kotlin 不会生成通配符

例如如下 Kotlin 程序

open class Base
class Sub : Base()
class Box<out T>(val value: T)

fun boxSub(value: Sub): Box<Sub> = Box(value)
fun unboxBase(box: Box<Base>): Base = box.value

编译上面程序,可以看到编译器生成的两个函数的签名如下

public static final Box<Sub> boxSub(Sub)
public static final Base unboxBase(Box<? extends Base>)

除 Kotlin 默认的转换规则之外,Kotlin 可使用注解控制是否生成通配符

注解 说明
@JvmWildcard 可指定在编译器默认不生成通配符的地方强制生成通配符
@JvmSuppressWildcards 可指定在编译器默认生成通配符的地方强制不生成,不仅可修饰单个泛型形参,还可修饰整个声明(如函数或类),从而阻止编译器为整个类或函数中的声明处型变生成通配符(使用处协变)
//对返回值类型强制生成通配符
fun boxSub(value: Sub): Box<@JvmWildcard Sub> = Box(value)

上面函数编译之后,生成函数的签名如下:

public static final Box<? extends Sub> boxSub(Sub)
//对返回值类型强制不生成通配符
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value

上面函数编译之后,生成函数的签名如下:

public static final Base unboxBase(Box<Base>)

Kotlin 反射

使用 Kotlin 的反射 API 需要添加单独的 JAR 文件: kotlin-reflect.jar ,因此可以添加依赖(版本与 kotlin 一致):

implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"

类引用

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 对象所对应类的详细信息

//定义注解
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) {
        Log.d("TAG", ": 执行有参数的构造器 name=$name")
    }

    //定义一个无参数的 info 方法
    fun info() {
        Log.d("TAG", "info: 执行无参数的 info()")

    }

    //定义一个有参数的 info 方法
    fun info(str: String) {
        Log.d("TAG", "info: 执行有参数的 info() str=$str")
    }

    //定义一个测试用的嵌套类
    class Inner
}

//为 ClassTest 定义扩展方法
fun ClassTest.bar() {
    Log.d("TAG", "bar: 扩展的 bar 方法")
}

//为 ClassTest 定义扩展属性
val ClassTest.foo: Double
    get() = 2.4

使用反射获取上面 ClassTest 的信息:

//下面代码可以获取 ClassTest 对应的 KClass
val clazz = ClassTest::class
//通过 constructors 属性获取 KClass 对象所对应类的全部构造器
val ctors = clazz.constructors
Log.d("TAG", "onCreate: ClassTest 的全部构造器如下:")
ctors.forEach {
    Log.d("TAG", "onCreate: $it")
}
Log.d("TAG", "onCreate: ClassTest 的主构造器: ${clazz.primaryConstructor}")
//通过 functions 属性获取该 KClass 对象所对应类的全部方法
var funs = clazz.functions
Log.d("TAG", "onCreate: ClassTest 的全部方法如下:")
funs.forEach {
    Log.d("TAG", "onCreate: $it")
}
// 通过 declaredFunctions 属性获取该 KClass 对象
// 本身所声明的全部方法(不包括继承的方法)
var funs2 = clazz.declaredFunctions
Log.d("TAG", "onCreate: ClassTest 本身声明的全部方法如下:")
funs2.forEach {
    Log.d("TAG", "onCreate: $it")
}
//通过 declaredMemberFunctions 属性获取该 KClass 对象
//本身所声明的全部成员方法(不包括继承的方法)
var memberFunctions = clazz.declaredMemberFunctions
Log.d("TAG", "onCreate: ClassTest 本身声明的成员方法如下:")
memberFunctions.forEach {
    Log.d("TAG", "onCreate: $it")
}

//通过 memberExtensionFunctions 属性获取该 KClass 对象
//所代表类的全部扩展方法(不包括继承的方法)
var extensionFunctions = clazz.memberExtensionFunctions
Log.d("TAG", "onCreate: ClassTest 本身声明的扩展方法如下:")
extensionFunctions.forEach {
    Log.d("TAG", "onCreate: $it")
}

//通过 declaredMemberProperties 属性获取该 KClass 对象
//本身所声明的全部成员属性(不包括继承的属性)
var memberProperties = clazz.declaredMemberProperties
Log.d("TAG", "onCreate: ClassTest 本身声明的成员属性如下:")
memberProperties.forEach {
    Log.d("TAG", "onCreate: $it")
}
//通过 memberExtensionProperties 属性获取该 KClass 对象
//所代表类的全部扩展属性(不包括继承的属性)
var extensionProperties = clazz.memberExtensionProperties
Log.d("TAG", "onCreate: ClassTest 本身声明的扩展属性如下:")
extensionProperties.forEach {
    Log.d("TAG", "onCreate: $it")
}
//通过 annotations 属性获取该 KClass 对象所对应类的全部注解
val anns = clazz.annotations
Log.d("TAG", "onCreate: ClassTest 的全部注解如下:")
anns.forEach {
    Log.d("TAG", "onCreate: $it")
}
Log.d(
    "TAG", "onCreate: ClassTest 该 KClass 元素上的" +
            " @Anno 注解为:${clazz.findAnnotation<Anno>()}"
)
//通过 nestedClasses 属性获取该 KClass 对象
// 所对应类的全部嵌套类(包括内部类和嵌套类)
val inners = clazz.nestedClasses
Log.d("TAG", "onCreate: ClassTest 的全部内部类如下:")
inners.forEach {
    Log.d("TAG", "onCreate: $it")
}
// 通过 supertypes 属性获取该类的所有父类型(包括父类和父接口)
Log.d("TAG", "onCreate: ClassTest 的父类型为:${clazz.supertypes}")

虽然定义 ClassTest 类时使用了 @Suppress 注解,但程序运行时无法分析出该类中包含的该注解,这是因为@Suppress 使用了@Retention(SOURCE)修饰,这表明 @Suppress 只能保存在源代码级别上,而通过 ClassTest.class 获取的是该类的运行时 KClass 对象,所以程序无法访问到 @Suppress 注解

通过 KClass 对象可以得到大量的 KFunction、 KProperty (它们都是 KCallable 的子类)等对象,这些对象分别代表该类所包括的方法(包括构造器)和属性等,程序还可以通过这些对象来执行实际的功能,例如调用方法、创建实例等

创建对象

获取 KClass 象之后,调用该对象的 createlnstance() 方法即可创建该类的实例,该方法总是调用 KClass 所代表类的无参数的构造器来创建实例

如果需要调用有参数的构造器来创建实例,则可通过 KClass 的 constructors 属性来获取所有构造器,该属性返回 Collection<Function>集合对象,这意味着构造器的本质依然是一个函数

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
    }
}

利用反射创建 Item 类的实例

val clazz = Item::class
// createInstance() 方法调用无参数的构造器创建实例
val inst1 = clazz.createInstance()
Log.d("TAG", "onCreate: inst1 name=${inst1.name},price=${inst1.price} ")
//获取所有构造器
val cons = clazz.constructors
cons.forEach {
    if (it.parameters.size == 2) {
        //调用带两个参数的构造器创建实例
        val inst2 = it.call("kotlin", 4.5)
        Log.d("TAG", "onCreate: inst2 name=${inst2.name},price=${inst2.price} ")
    }
}

构造器引用

构造器的本质是一个函数,即一个返回值为当前类实例的函数,因此,程序可将构造器引用当成函数使用

此外, Kotlin 允许通过使用“::”操作符并添加类名来引用该类的主构造器

class Foo(var name: String = "kotlin")

// test 函数的参数是 (String) -> Foo 类型(这就是 Foo 带 String 参数的构造器的类型)
fun test(factory: (String) -> Foo) {
    val x: Foo = factory("kotlin")
    Log.d("TAG", "test: name=${x.name}")
}

//使用
//通过 ::Foo 引用 Foo 类的主构造器
test(::Foo)

如果要获取 Kotlin 造器引用对应的 Java 造器对象( Constructor ),则可通过调用 KFunction 的扩展属性 javaConstructor 来实现

::Foo.javaConstructor

需要说明的是,如果要调用构造器引用的 javaConstructor属性,则需要导入 kotlin.reflect.jvm 包,因为这些扩展属性都属于与 Java 反射互相操作的部分,被定义在 kotlin.reflect.jvm 下

调用方法

所有构造器和方法都属于 KFunction 实例,因此它们都可以通过 call() 方法来调用

class Foo {
    fun test(msg: String) {
        Log.d("TAG", "test: 执行带 String 参数的 test 方法 msg=$msg")
    }

    fun test(msg: String, price: Double) {
        Log.d("TAG", "test: 执行带 String, Double 参数的 test 方法 msg=$msg ,price=$price")
    }
}

反射调用方法

val clazz = Foo::class
//创建 Foo 类的实例
val ins = clazz.createInstance()
//获取 clazz 所代表类直接定义的全部函数
val funs = clazz.declaredFunctions
for (f in funs) {
    //如果函数具有3个参数(对应带2个参数的方法)
    if (f.parameters.size == 3) {
        //调用带3个参数的函数
        f.call(ins, "Kotlin", 78.8)
    }
    //如果函数具有2个参数(对应带1个参数的方法)
    if (f.parameters.size == 2) {
        //调用带2个参数的函数
        f.call(ins, "Kotlin")
    }
}

函数引用

Kotlin 程序可以获取函数的引用,把函数当成参数传入另一个函数中,Kotlin 也通过“::”符号加函数名的形式来获取特定函数的引用。当存在多个重载函数时,Kotlin 可通过上下文推断出实际引用的是哪个函数,如果 Kotlin 无法通过上下文准确推断出引用哪个函数,编译器就会报错

//定义两个重载的函数
fun isSmall(i: Int) = i < 5
fun isSmall(s: String) = s.length < 5

//使用
val list = listOf(20, 30, 100, 4, -3, 2, -12)
//由于 filter() 函数需要 (Int)-> Boolean 类型的参数,
//故此处::isSmall 引用第一个函数
val resultList = list.filter(::isSmall)
Log.d("TAG", "onCreate: resultList=$resultList")
val strlist = listOf("kotlin", "java")

//由于 filter 函数需要 (String)-> Boolean 类型的参数,
//故此处::isSmall 引用第二个函数
val resultStrList = strlist.filter(::isSmall)
Log.d("TAG", "onCreate: resultStrList=$resultStrList")
//无法推断出 ::isSmall 到底引用哪个函数,报错
//val f =::isSmall
//可以推断出::isSmall 到底引用哪个函数,正确
var f: (String) -> Boolean = ::isSmall
Log.d("TAG", "onCreate: f()=${f("kotlin")}")

如果需要引用类的成员方法或扩展方法,那么需要进行限定。例如 String::toCharArray 能表明引用 String 的 toCharArray() 方法,单纯地使用 ::toCharArray() 不行

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)) }
}

comp() 函数将 ::abs 和 ::sqrt 两个函数组合在一起,这样就会得到一个新的函数 f, 接下来程序可通过 f() 函数同时完成两个函数的功能

如果要获取 Kotlin 函数引用对应的 Java 方法对象( Method ),则可通过调用KFunction 的扩展属性 javaMethod 来实现

::abs.javaMethod

访问属性值

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 Item {
    var name: String = "kotlin"
    val price: Double = 4.6
}

访问修改属性值:

val clazz = Item::class
val ins = clazz.createInstance()
val props = clazz.declaredMemberProperties
props.forEach {
    when (it.name) {
        "name" -> {
            //将属性转换为读写属性
            val mp = it as KMutableProperty1<Item, Any>
            //修改属性值
            mp.set(ins, "java")
            Log.d("TAG", "onCreate: ${it.get(ins)}")
        }
        "price" -> {
            //只读属性,只能通过 get() 方法读取属性值
            Log.d("TAG", "onCreate: ${it.get(ins)}")
        }
    }
}

属性引用

Kotlin 同样提供了“::”符号加属性名的形式来获取属性引用

var foo = "测试属性"

class Item {
    var name: String = "kotlin"
    val price: Double = 4.6
}

使用属性引用

//获取 foo 属性,属于 KMutablePropertyO 的实例
val topProp = ::foo
topProp.set("修改后的属性")
Log.d("TAG", "onCreate: ${topProp.get()}")

val im = Item()
//获取 Item 的 name 属性,属于 KMutableProperty1 的实例
val mp = Item::name
mp.set(im, "java")
Log.d("TAG", "onCreate: ${mp.get(im)}")

//获取 Item 的 price 属性,属于 KProperty1 的实例
val prop = Item::price
Log.d("TAG", "onCreate: ${prop.get(im)}")

Kotlin 在 kotlin.reflect.jvm 包下也提供了 Kotlin 属性与 Java 反射互操作的扩展属性,由于 Kotlin 属性会对应于 Java 种成员,因此 KProperty 包含如下 3 个扩展属性

扩展属性 说明
javaField 获取该属性的幕后宇段(如果该属性有幕后字段的话) 该属性返回java.lang.reflect.Field 对象
javaGetter 获取该属性的 getter 方法 该属性返回 java.lang.reflect.Method 对象
javaSetter 获取该属性的 setter 方法(如果该属性是读写属性的话) 该属性返回java.lang.reflect.Method 对象

绑定的方法与属性引用

前面介绍的都是通过 KClass (类本身)来获取方法或属性的引用的,当函数或属性不在任何类中定义时,程序直接使用“::”加函数名(或属性名)的形式来获取函数或属性的引用,这些函数或属性都没有绑定任何对象,因此调用函数或属性时第一个参数必须传入调用者

Kotlin 1.1 开始, Kotlin 支持一种“绑定的方法或属性引用”,这种方法或属性引用不是通过类获取的,而是通过对象获取的,这意味着该方法或属性己经绑定了调用者,因此程序执行这种方法或属性时无须传入调用者

val str = "kotlin"
//获取对象绑定的方法
val f: (CharSequence, Boolean) -> Boolean = str::endsWith
//调用绑定的方法时无须传入调用者
Log.d("TAG", "onCreate: ${f("java", true)}")
//获取对象绑定的属性
val prop = str::length
//调用绑定的属性时无须传入调用者
Log.d("TAG", "onCreate: ${prop.get()}")
var list = listOf("kotlin", "java")
//获取对象绑定的方法
val fn = list::subList
//调用绑定的方法时无须传入调用者
Log.d("TAG", "onCreate: ${fn(1, 1)}")
//获取对象绑定的属性
val prp = str::indices
//调用绑定的属性时无须传入调用者,输出 0..5
Log.d("TAG", "onCreate: ${prp.get()}")
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容