kotlin与java互操作中的冲突

Posted 疯狂小芋头

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kotlin与java互操作中的冲突相关的知识,希望对你有一定的参考价值。

2021-07-09


以下基于kotlin 1.5.20 版本

1. kotlin中的属性与方法

在kotlin中的公共属性默认会被编译成为相应的gettersetter方法在java中可以去被调用。

在编译后的JVM字节码中,公共属性被编译成指定的gettersetter签名的方法,所以在java中使用该属性是通过方法的方式进行使用的,而不是字段。

//在java中将被编译成getName()与setName(String)的方法签名
var name:String=""

1.1. JVM签名冲突

由于kotlin编译时会有这样的机制,所以以下代码就会出现问题。

interface IStudent{
    //定义了一个只读属性name
    val name:String

    //定义了一个getName的方法
    //此处会报错
    fun getName():String
}

此处会报错 Platform declaration clash: The following declarations have the same JVM signature (getName()Ljava/lang/String;)

由于只读属性name最终会被编译成String getName()这样的方法签名,所以实际上与下面定义的方法已经产生了签名冲突了。

val name:String -> 编译成:String getName()
 fun getName():String -> 编译成:String getName()
//这里编译后已经存在相同签名的方法了

对于这样的情况,意味着有以下限制:

  1. 在kotlin中,同一个类/接口使用了只读属性(val)则不能定义相应的getter方法;使用了可读写属性(var)则不能定义相应的gettersetter方法
  2. 在kotlin中,子类继承的父类/接口也不能出现冲突的属性或方法
  3. 同理在java与kotlin混合使用的互操作情况下,不管在java中继承kotlin的类/接口,还是反过来,都双方都不允许出现有冲突的属性或方法

类似问题参考:
How to overcome “same JVM signature” error when implementing a Java interface?

1.2. 可能的解决方案

以上的问题根本原因已经提及了,就是因为最终的签名冲突了。所以只要能解决签名的问题,那么就不会存在此问题了。

1.2.1. 修改签名

幸运的是,在kotlin中允许通过注解修改编译后的方法名

class B{
    //最后会被编译成方法:
    //String getBName()
    @JvmName("getBName")
    val name:String
}

而且对于可变属性,由于存在gettersetter,这个注解还能贴心地分别提供不同的名称

class B{
    //这里属性的获取与设置方法名称是不同的
    @get:JvmName("getBName")
    @set:JvmName("changeBName")
    var name:String
}

1.2.2. 映射为字段

除了修改方法的签名,还有一种方式是将冲突的字段编译成字段。

class B{
    //编译为后等同于
    //public final String name=""
    @JvmField
    val name:String=""
    //这里不会有冲突提示
    fun getName():String
}

这种情况下会将属性编译成字段,而不再是方法,那就不会存在签名冲突了

1.2.3. 局限性及注意事项

虽然有相关的解决方案,但是该操作依然有局限性/注意事项。

  1. 实测接口中无法使用这个注解修改属性或方法名称,也无法将属性编译为字段
interface C{
    //编译不通过
    @JvmName("getCName")
    val name:String
}

此处报错提示:’@JvmName’ annotation is not applicable to this declaration

This annotation is not applicable to target ‘member property without backing field or delegate’

interface C{
    @JvmField
    val name:String
}

此处报错提示:This annotation is not applicable to target ‘member property without backing field or delegate’

  1. 在kotlin中使用了此注解,不影响在kotlin中调用相应的属性和方法,依然会使用声明时的名称而不是注解修改后的名称

1.3. 潜在的问题

1.3.1. 第三方库的使用限制

由于以上的限制,在正常情况下,当一个库在实现过程中,即使不小心出现以上冲突也可以由编译器发现并修改以解决问题。在使用其它库时,也同样可能出现以上问题,但依然可以通过编译器发现并修改。所以正常情况下,一般不会有很大的影响。

但是,有一种情况是无法避免以及补救的。如果存在类A,继承自类B(第三方的库),同时需要实现类C(第三方的库,与B所属库是否相同均可),若B与C同时存在冲突的签名,则无法解决此问题。

//第三库
open class B{
    val name:String
}

interface C{
    fun getName():String
}

//当前项目
class A :B(),C{
    //编译器报错,因为同时存在两个jvm签名冲突的情况
}

通过个人的探索得出结论:此问题在目前无法补救在这种情况,要不放弃掉继承父类B,要不放弃掉实现接口C,只能二选一

注意这里的核心点是:对于同时继承的父类/接口,我们无法去对类进行修改(即已经编译后的类),如果存在源码,依然可以通过其它解决方案处理此问题。

1.3.2. 独特的枚举问题

以上的问题也会导致在枚举中引起其它问题。

对于一个java类的枚举,我们可以有以下的实现方式。

public enum A {
    INTSTANCE("happy");
    public String name;
    A(String name) {
        this.name = name;
    }
}

该类在java中可以正常使用,在kotlin中被也可以被正常使用。但是我们将无法取到name这个字段(如果不考虑万能的反射)。如果在kotlin中使用时

//这里只能取到枚举对象的名称,即INSTANCE
val name=A.INTSTANCE.name

因为在java中定义的字段,会被编译成对应的getter方法,所以原本的name字段将会变成String getName()
而在kotlin中,使用该方法时,会转换成对name属性的使用,所以最终在正常情况下使用该字段将会是直接使用对应名称的属性。

但是,枚举类本身存在一个固定方法name(),用于获取当前枚举对象的名称。而在kotlin中这不再是一个方法而是一个只读属性(kotlin中的枚举名称),所以就会造成同名了。

参考问题:How to get name from Java enum in Kotlin

严格来说这个并不是kotlin的问题,因为在枚举中使用name这个字段其实并不是很合理,即使在java中也很容易与原来自带的name()方法造成混淆。所以引用网友的一段话:

If possible, I recommend changing the name of the Java field and getter to something other than name and getName(). Personally, I think it’s a bad idea to have a field named name in a Java enum anyway, considering the existence of the name() method.
我建议修改java字段的名称而不是使用name的字段名称或者是getName()的方法名称。鉴于原来已经存在的name()的方法,个人觉得为java中的枚举类命名一个name字段是一个坏主意

但是在kotlin中定义枚举时不需要担心该问题,因为编译器会直接提示有同签名的错误,所以我们是无法在kotlin代码中定义一个name属性的

1.4. 扩展思考

我们可以大胆猜测,由于kotlin考虑到与java的互操作性,并且会将kotlin中的属性与java中的方法对应起来,才会出现这种的问题。那么为什么kotlin不直接将属性编译成字段呢?

我们知道kotlin里的属性实际上远比字段更强大,我们可以实现以下的功能

class B {
    private var _name = ""

    //给name属性设置的值都会被内部私有属性_name存储
    //而获取name属性的值时,永远是happy
    var name: String
        set(value) {
            _name = value
        }
        get() = "happy"
}

在这种情况下,实际上属性已经有点扮演了方法的角色了。而在java中字段就是字段,单纯是存储某些数据。为了保证kotlin与java的互操作性,确保java调用了kotlin的代码能正常运行而不会出现预期以外的问题,那么这里也只能将属性编译成方法给java调用了。

因此,在以上属性实现中,@JvmField就不再适用了,实际上在编译时为以上属性添加该注解编译器会直接报错的,因为这个属性已经无法简单地映射成java中的一个字段了

1.5. 小结

  1. 在kotlin中无法同时定义同名的属性及与之映射成java中方法相同签名的方法
  2. 在kotlin中无法同时继承/实现具有签名冲突的类/接口
  3. 在java的枚举中,应该避免使用name字段或getName()这样的方法,避免kotlin中使用的问题

以上是关于kotlin与java互操作中的冲突的主要内容,如果未能解决你的问题,请参考以下文章

kotlin与java互操作中的冲突

kotlin与java互操作中的冲突

Kotlin的互操作——Kotlin与Java互相调用

KotlinKotlin 与 Java 互操作 ③ ( Kotlin 中处理 Java 异常 | Java 中处理 Kotlin 异常 | @Throws 注解处理异常 | 函数类型互相操作 )

深入kotlin - 与Java互操作:kotlin调用java

KotlinKotlin 与 Java 互操作 ① ( 变量可空性 | Kotlin 类型映射 | Kotlin 访问私有属性 | Java 调用 Kotlin 函数 )