Kotlin和Java互相调用(一)

Kotlin 调用 Java

由于 Kotlin本身并没有提供强大的类库支持,Kotlin只是一种语言,因此 Kotlin 调用 Java 通常都是自然而然的事情。正如我们在前面程序中所看到的,在 Kotlin 中调用Java 的 Date、 JFrame 等类完全没有任何问题 。
Kotlin调用 Java时只是有一些小小的注意点,下面对这些内容做一下简单的说明。

属性

Kotlin调用属性实际上就是访问 getter、 setter方法,因此Java类只要提供了 getter方法, Kotlin就可将其当成只读属性 ;如果 Java 类提供了 getter、setter 方法,Kotlin 就可将其当成读写属性。有没有这个成员变量已经不重要了。

如果 getter方法的返回值类型是 boolean,该getter方法名也以is开头,此时Kotlin会将其当成属性名与 getter方法同名的属性。比如 boolean isMarried()方法可作为 Kotlin 的只读属性,该属性名为 isMarried。

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

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

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

public class InMethod {
    public void  in(){
        System.out.println("in");
    }

}
fun main(args: Array<String>) {
    val im = InMethod()
    //对 in 关键字转义
    im.`in`()
}

Kotlin 的己映射类型

表 12.1 显示了 Java基本类型与 Kotlin类之间的映射关系。


image.png

Java基本类型的包装类则映射到 Kotlin类的可空类型,如表 12.2所示。


image.png

此外, Kotlin还为 Java的一些常用类提供了映射,如表 12.3所示。
image.png

Kotlin 对 Java 泛型的转换

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


image.png

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

fun main(args: Array<String>) {
    val list = listOf(2,3,10)
    //is判断不能检测泛型参数
   // println(list is List<String>)
    //只能检测星号投影
    //输出true
    println(list is List<*>)
}

正如从上面程序中所看到的,程序检测 list is List<String>是无效的,这是因为程序在运行时只能判断是否为 List,无法判断它的泛型信息。

对Java数组的处理

相比 Java数组,Kotlin数组有一个重大的改变:Kotlin数组是不型变的。因此 Array<String>不能赋值给 Array<Object>变量。 但 Java 数组不同,Java 数组是型变的,因此 String[]可以直接赋值给 Object[]变量。

此外, Java还支持 int[]、 long[]等基本类型的数组,这种数组可以避免拆箱、装箱带来的性能开销,但 Kotlin 的 Array 类不支持这种基本类型的数组。为此, Kotlin 提供了 ByteArray、 ShortArray、 IntArray、 LongArray、 CharArray、 FloatArray、 DoubleArray、 BooleanArray数组, 这几种数组专门用于代替 Java的 byte[]、short[]等基本类型的数组。比如定义了如下 Java方法。

public class InMethod {
    public int sum(int [] array) {
        int sum =0;
        for (int i = 0; i < array.length; i++) {
            sum+=array[i];
        }
        return sum;
    }
}

对应的kotlin代码

class InMethod {
    fun sum(array: IntArray): Int {
        var sum = 0
        for (i in array.indices) {
            sum += array[i]
        }
        return sum
    }
}

此外 ,读者无须对 Kotlin数组访问效率有任何担心, Kotlin编译器会对数组访问进行优化, 它会采用和 Java 相似的方式来访问数组元素 Java 访问数组元素的方式是:根据索引直接计算数组元素的内存地址,访问效率非常高,因此 Kotlin 在访问元素时没有性能损耗 。

fun main(args: Array<String>) {
    val array = arrayOf(1, 2, 3, 4)
    //不会使用 get ()、 set ()方法访问数组元素
    ///实际上还是通过 Java 数组的快速访问
    array[1] = array[1] * 2

    //使用索引定位是基于 Java 数组元素所在内存地址的快速访问
    //不需要创建迭代器
    for (i in array.indices) {
        array[i] += 2

    }

    //for-in 循环也是基于 Java 数组元素所在内存地址的快速访问
    //不需要创建迭代器
    for (i in array) {
        println(i)
    }
}

正如从上面程序中所看到的,不管程序采用哪种方式来遍历数组的元素, Kotlin总会将其优化成和 Java类似的访问数组元素的方式,因此开发者无须担心性能问题。

调用参数个数可变的方法

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

public class Test {
    public void test(int... nums) {
       //定义参数个数可变的方法
        for (int i = 0; i < nums.length; i++) {
            System.out.println(nums[i]);
        }
    }
}

对于上面的 test()方法,程序可直接传入多个 int 值来调用该方法;但如果程序己有一个IntArray数组,则需要使用“*”将数组解开成多个数组元素传入 。

fun main(args: Array<String>) {
    val test = Test()
    val intArray = intArrayOf(1,3,5,7,9)
    //将数组解开成多个元素
    test.test(*intArray)
}

checked 异常

由于 Kotlin没有 checked 异常,因此对于 Java 中可能引发 checked 异常的方法、构造器 , Kotlin 则不会引发该异常, Kotlin 既可捕获该异常,也可完全不理会该异常(无须使用 throws 声 明抛出异常)。例如如下程序。

fun main(args: Array<String>) {
    //下面代码可能引发 IOException (checked 异常)
    //fl. Kotlin 并不强制处理该异常
    val fos = FileInputStream("texr.txt")
    println(fos.read())
}

Object 的处理

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

1. wait() / notify() / notifyAll()

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

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

2. getClass()

Object 对象的 getClass()方法用于获取该对象的 Java 类, Kotlin 的对象则有两种方式来获取该对象的 Java类。例如如下代码:

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

obj.javaClass

3. clone()

如果要让对象重写 cloneO方法,则需要让该类实现kotlin.Cloneable接口 。例如如下代码:

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

4. finalize()

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

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

访问静态成员

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

fun main(args: Array<String>) {
//调用 Runtime 的静态方法(就像调用伴生对象的方法一样〉
    val rt = Runtime.getRuntime()
    println(rt)
    // 访问 java . awt .BorderLayout 的 NORTH 静态成员〈就像访问伴生对象的属性一样)
    val str =java.awt.BorderLayout.NORTH
    println(str)
}

SAM转换

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

fun main(args: Array<String>) {
    //使用 Lambda 表达式来创建函数式接口(Predicate)的对象
    val pred = Predicate<Int> { t -> t > 5 }
    val list = arrayListOf(3, 5, 7, 200)
    //使用 pred 对 List 集合进行过滤
    //输出[3, 5]
    list.removeIf(pred)
    println(list)


    //使用 Lambda 表达式来创建函数式接口( Runnable)的对象
    val rn = Runnable {
        for (i in 0..10) {
            println(i)
        }
    }

    //通过 Runnable 对象创建 、 启动线程
    Thread(rn).start()

    val excutor = ThreadPoolExecutor(
            5,
            10,
            0L,
            TimeUnit.MILLISECONDS,
            LinkedBlockingQueue<Runnable>()
    )

    //由于 executor 的 execute ( ) 方法需要 一个 Runnable 对象
    //因此程序可自动将符合该接口规范的 Lambda 表达式创建成 Runnable 对象
    excutor.execute {
        println("This runs in a thread pool")
    }

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

}

在 Kotlin 中使用 JNI

如果要在 Java 中使用JNI,则应该使用 native 修饰该方法,表明该方法将会交给平台本地的C或 C++代码来实现 。
Kotlin 同样支持该机制,只不过 Kotlin 不使用native关键字,而是使用external 关键字。例如如下函数:

external fun foo(x: Int): Double

该函数使用了 external修饰,因此该函数不能指定函数体。 该函数的函数体将会由平台本地的C 或 C++代码来实现。

Java调用 Kotlin

由于 Kotlin程序编译之后本身就是完全兼容JVM规范的,因此 Java调用 Kotlin也是非常方便的 。只是有一些小的注意点。

属性

前面在介绍Kotlin属性时己经讲过, Kotlin属性可编译成如下三个成员。

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

例如如下属性:

var name: String

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

    private String name;
    
    public String getName() {
        return name;
    }

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

但如果属性名以is开头(属性类型并不要求一定是Boolean类型〉, 那么该属性对应的生成规则略有差别。

  • 实例变量名 、 getter方法名与属性名相同。
  • setter方法名为属性名去掉 is 前缀,添加 set前缀。

包级函数

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

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

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

public class TopLevelKt {
    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 的顶级成员编译之后的形式,读者自然也就知道如何调用它们了。在默认情况下, Kotlin 为包含顶级成员的源文件所生成类的类名总是文件名+Kt 后缀,但 Kotlin也允许使用@JvmName 注解(该注解用来修饰文件本身,它是一个 AnnotationRetention.SOURCE 的注解)来改变 Kotlin 编译生成的类名。

//指定生成的类
@file:JvmName ("FkUtils")
package org.crazyit

fun test() {}

上面代码指定该源文件所生成的类名为FkUtils, 因此编译该源文件将会生成一个 FkUtils.class文件。

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

//指定生成的类
@file:JvmName("CiUtils")
@file:JvmMultifileClass
package test11

fun foo1(){
    println("1111")
}

Demo3.kt中

//指定生成的类
@file:JvmName("CiUtils")
@file:JvmMultifileClass
package test11

fun foo2(){
    println("2222")
}

上面两个 Kotlin 源程序都需要生成 test11.CiUtils 类,在默认情况下这将导致错误。因 此,上面程序使用了@JvmMultifileClass注解,这就会告诉 Kotlin编译器将两个文件合并到一个类中。

接下来 Java 程序可采用如下方式调用里面的方法。

public static void main(String[] args){
    CiUtils.foo1();
    CiUtils.foo2();
}

实例变量

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

  • 该属性具有幕后字段。
  • 该属性必须是非 private访问控制的。
  • 该属性不能使用 open、 override、 const修饰。
  • 该属性不能是委托属性。

例如如下 Kotlin程序。

class InstanceField (name : String) {
      @JvmField val myName = name
}

上面代码定义了一个属性,并使用@JvmField注解修饰了该属性,这样系统就会把该属性的幕后字段暴露成实例变量(默认是 public 访问权限)。

下面 Java程序示范了访问该类的对象的实例变量。

    public static void main(String[] args) {
        InstanceField ins = new InstanceField ("Kotlin");
        //访问 InstanceField对象的实例变量
        ins.myName;
    }

类变量

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

  • 使用@JvmField注解修饰。
  • 使用 lateinit 修饰。
  • 使用 const 修饰。

如下程序在 MyClass 类中定义了一个伴生对象。

class MyClass{
    //使用 companion 修饰的伴生对象
    companion object MyObject{
        @JvmField val name ="name属性值"
    }
}

上面代码在伴生对象中定义了一个属性。根据前面的介绍我们知道:伴生对象本来就是用来弥补 Kotlin 没有 static 关键字的不足的,因此伴生对象中的属性实际上就相当于 MyClass 的类变量,但它默认是 private 访问权限 。上面代码使用了@JvmField 修饰,这样该类变量就变成了与该属性具有相同的访问控制符: public。

    public static void main(String[] args) {
        //访问 MyClass 的 name 类变量
        System.out.println(MyClass.name);
    }

在命名对象或伴生对象中的延迟初始化属性 (lateinit)具有与该属性的 setter 方法相同的访问控制符的类变量。道理很简单: Java 并不支持命名对象,因此 Kotlin 的命名对象在 Java 中其实表现为一个单例类,命名对象的属性就变成了类变量:又由于 lateinit属性需要等到使用时才赋值,因此 Kotlin必须将该类变量暴露出来,否则 Java程序将无法对该类变量赋值。

class MyClass {
    //定义命名对象
    object MyObject{
        //定义延迟初始化属性
        lateinit var name: String
    }
}

上面程序在 MyObject 命名对象中定义了延迟初始化属性,该属性将会被暴露成类变量, 因此 Java程序可按如下方式来调用它。

    public static void main(String[] args) {
        //访问 MyClass 的 name 类变量
        MyClass.MyObject.name = "sss";
        System.out.println(MyClass.MyObject.name);
    }

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

const val MAX =100

object Obj{
    const val name ="kotlin"
}

class MyClass{
    companion object{
        //使用 const修饰的变量
        const val age =14
    }
}

上面程序中定义了一个顶层的const属性,还在命名对象中定义了一个 const 属性,在伴生对象中定义了 一个 const属性,这些 const属性都会变成有public static final 修饰的属性。因此可用如下Java程序来调用它们。

    public static void main(String[] args) {
        System.out.println(MyClass.age);
        System.out.println(MyClassKt.MAX);
        System.out.println(Obj.name);
    }

类方法

可以肯定的是, Kotlin 的顶级函数会被转换成类方法。
此外, Kotlin 还可以将命名对象或伴生对象中定义的方法转换成类方法一一如果这些方法使用@ JvmStatic 修饰的话。一旦使用该注解,编译器就既会在相应对象的类中生成类方法,也会在对象自身中生成实例方法。例如如下 Kotlin程序。

//指定零个父类型的命名对象
object MyObject {
    //方法
    fun test() {
        println("test()")
    }

    @JvmStatic
    fun foo() {
        println("有@JvmStatic修饰的foo()")
    }
}

class MyClass {
    //省略名字的伴生对象
    companion object {
        //方法
        fun test() {
            println("test()")
        }

        @JvmStatic
        fun output(string: String) {
            println(string)
        }
    }
}

上面程序中定义了一个命名对象和一个伴生对象,其中命名对象中包含了两个方法: 一个方法没有 @JvmStatic 修饰,另一个方法有@JvmStatic 修饰:伴生对象中也包含了两个方法: 一个方法没有@JvmStatic 修饰,另 一个方法有@JvmStatic 修饰。这样有@JvmStatic 修饰的方法既会在相应对象的类中生成类方法,也会在对象自身中 成实例方法。例如,可使用如下Java代码来调用这些方法。

    public static void main(String[] args) {
        //通过 INSTANCE 访问 MyObject 的单例,通过单例访问 test ()方法
        MyObject.INSTANCE.test();
        //错误, test ()方法只是单例对象的实例方法
        //MyObject.test();
        //通过 INSTANCE 访问 MyObject 的单例,通过单例访问 foo ()方法
        MyObject.INSTANCE.foo();
        //正确, foo ()方法有自JvmStatic 修饰,因此也是 MyObject 类的类方法
        MyObject.foo();

        //通过 Companion 访问 MyClass 的伴生对象,通过伴生对象访问 test ()方法
        MyClass.Companion.test();
        //错误, test ()方法只是伴生对象的实例方法
        //MyClass.test();
        //通过 Companion 访问 MyClass 的伴生对象,通过伴生对象访问 output ()方法
        MyClass.Companion.output("sss");
        //正确, output ()方法有自 JvmStatic 修饰,因此也是 MyClass 类的类方法
        MyClass.output("ccc");
    }

从上面程序中可以看出,对于命名对象中有@JvmStatic 修饰的方法,既可通过命名对象的时STANCE (引用单例对象)来调用该方法,也可通过命名对象对应的类来调用该方法; 对于伴生对象中有 @JvmStatic 修饰的方法,既可通过伴生对象所在类的 Companion (引用伴生对象)来调用该方法,也可通过伴生对象所在的类来调用该方法。

访问控制符的对应关系

Kotlin 的访问控制符与 Java 的对应关系如下 。

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

获取 KClass

前面介绍过使用 Kotlin 来获取 Java 的 Class 对象:反过来,有时候 Java 也需要获取 Kotlin 的 KClass 对象。为了在 Java 中获取 Kotlin的KClass 对象,必须通过调用 Class<T>.kotlin 扩展属性的等价形式来实现。

    public static void main(String[] args) {
        try {
            Class<?> clazz = Class.forName ("java.util.ArrayList" );
            System.out.println(clazz);
            //获取 Class 对象的 KClass 对象
            KClass kc = JvmClassMappingKt.getKotlinClass(clazz);
            System.out.println(kc);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

使用@ 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("fileterValidint")
fun List<Int>.filterValid(): List<Int> {
    val result = mutableListOf<Int>()
    for (i in this) {
        if (i < 20) {
            result.add(i)
        }
    }
    return result.toList()
}

上面程序中定义了两个扩展方法,虽然程序看上去是分别为 List<String>、 List<Int>扩展的方法,但由于编译器编译之后会产生类型擦除,因此上面两个方法的签名都是List.filterValid():List。 所以在JVM平台上无法区分这两个方法。

为了让JVM平台能区分这两个方法,程序中代码使用了@JvmName("fileterValidlnt") 来标注后一个方法,因此后 一个方法在JVM平台上会编译成 fileterValidlnt()方法。

    public static void main(String[] args) {
        List<String> list1 = Arrays.asList ("java","kotlin");
        System.out.println(MyClassKt.filterValid(list1));

        List<Integer> list2 = Arrays.asList (111,222);
        System.out.println(MyClassKt.fileterValidInt(list2));
    }

从上面程序中可以看出,增加@JvmName 注解之后,程序就可以正常区分两个签名相同的方法了。

@JvmName注解也适用于属性x和函数 getX()共存。例如如下代码:

class MyClass{
    val x: Int
        @JvmName("getX_prop")
        get() = 15

    fun getX():Int{
        return 10
    }
}

生成重载

我们知道, Kotlin为方法(或函数)提供了参数默认值来避免函数重载过多的问题。但对于这种参数有默认值的方法(或函数),编译器默认只生成一个方法:默认带所有参数的方法 。
为了让编译器能为带参数默认值的方法(或函数)生成多个重载的方法(或函数),可考虑使用@JvmOverloads 注解 。
需要说明的是,@JvmOverloads 注解也适用于构造器、静态方法等。它不适用于抽象方法,包括在接口中定义的方法 。
如下程序示范了使用@JvmOverloads 注解让编译器为带参数默认值的方法生成多个重载的方法。

@JvmOverloads
fun test(name:String ,age :Int = 24,sex:String = "男"){
    println(name)
    println(age)
    println(sex)
}

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

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

checked 异常

前面已经介绍过, Kotlin没有 checked 异常,因此 Kotlin 也无须使用 throws 抛出异常。这样即使 Kotlin 的方法(或函数)本身可能抛出 checked 异常, Java 调用该方法(或函数)时编译器也不会检查该异常。
例如,如下函数就存在这个问题。

fun foo() {
    val fis = FileinputStream("a.txt")
}

如果我们希望 Java 调用该函数时编译器会检查该 IOException,则可使用@Throws 注解修饰该函数。例如,将上面代码改为如下形式。

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

这样上面 foo()函数就相当于声明了 throws IOException,因此 Java程序调用该函数时编译 器会检查该 checked 异常,程序要么捕获该异常,要么显式声明抛出该异常 。

泛型的型变

首先回顾一下 Kotlin 泛型型变和 Java 泛型型变的区别: Kotlin的泛型支持声明处型变,而Java的泛型只支持使用处型变(通过通配符形式,可以支持协变和逆变两种形式)。因此对于 Kotlin的声明处型变,必须转换成 Java的使用处型变。其转换规则是:

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

例如如下 Kotlin 程序 。

class Box<out T>(val value: T)
open class Base 
class Sub: Base()
fun boxSub(value : Sub) : Box<Sub> = Box(value)
fun unboxBase(box: Box<Base>): Base= box.value

上面程序中定义了一个支持声明处协变的 Box 类,接下来程序定义了两个函数,分别使用 Box<Base>作为参数类型,使用 Box<Sub>作为返回值类型。编译上面程序,可以看到编译器生成的两个函数的签名如下 :

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

从代码可以看出,由于 Box<out T>是声明处协变的泛型类,因此程序将它作为参数时,编译器自动使用了运行处协变来代替它。这样 Java程序调用 unboxBase()方法时既可传入Box<Base>作为参数,也可传入 Box<Sub>作为参数,只要尖括号中的类型是 Base 的子类即可。
除 Kotlin 默认的转换规则之外,Kotlin还可使用注解控制是否生成通配符。 Kotlin提供了如下两个注解。

  • @ JvmWildcard: 该注解可指定在编译器默认不生成通配符的地方强制生成通配符。
  • @JvmSuppressWildcards: 该注解可指定在编译器默认生成通配符的地方强制不生成通配符。

如下程序示范了@JvmWildcard 注解的用法。

class Box<out T>(val value: T)
open class Base
class Sub: Base()
//对返回值类型强制生成通配符
fun boxSub(value : Sub) : Box<@JvmWildcard Sub> = Box(value)
fun unboxBase(box: Box<Base>): Base= box.value

上面代码使用@JvmWildcard 修饰了该函数的返回值类型 : 将会为该返回值类型生成通配符。因此,上面两个函数编译之后,生成两个函数的签名如下 :

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

从上面代码可以看出,此处编译器为返回值类型也生成了通配符(默认是不会生成通配符的) 。

如下程序示范了@JvmSuppressWildcards 注解的用法 。

class Box<out T>(val value: T)
open class Base
class Sub: Base()
fun boxSub(value : Sub) : Box<Sub> = Box(value)
fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base= box.value

上面代码使用@JvmSuppressWildcards 修饰了该函数的形参类型: Box<Base>,因此编译器将不会为函数的形参类型生成通配符。 因此上面两个函数编译之后,生成两个函数的签名如下:

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

从上面代码可以看出,此处编译器没有为形参类型生成通配符(默认会生成通配符) 。

@JvmSuppressWildcards 注解不仅可修饰单个泛型形参,还可修饰整个声明(如函数或类),从 而阻止编译器为整个类或函数 中的声明处型变生成通配符(使用处协变)。

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

推荐阅读更多精彩内容