kotlin与java互操作中的冲突
Posted 疯狂小芋头
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了kotlin与java互操作中的冲突相关的知识,希望对你有一定的参考价值。
2021-07-09
文章目录
以下基于kotlin 1.5.20 版本
1. kotlin中的属性与方法
在kotlin中的公共属性默认会被编译成为相应的getter
与setter
方法在java中可以去被调用。
在编译后的JVM字节码中,公共属性被编译成指定的getter
与setter
签名的方法,所以在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()
//这里编译后已经存在相同签名的方法了
对于这样的情况,意味着有以下限制:
- 在kotlin中,同一个类/接口使用了只读属性(val)则不能定义相应的
getter
方法;使用了可读写属性(var)则不能定义相应的getter
与setter
方法 - 在kotlin中,子类继承的父类/接口也不能出现冲突的属性或方法
- 同理在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
}
而且对于可变属性,由于存在getter
与setter
,这个注解还能贴心地分别提供不同的名称
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. 局限性及注意事项
虽然有相关的解决方案,但是该操作依然有局限性/注意事项。
- 实测接口中无法使用这个注解修改属性或方法名称,也无法将属性编译为字段
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’
- 在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. 小结
- 在kotlin中无法同时定义同名的属性及与之映射成java中方法相同签名的方法
- 在kotlin中无法同时继承/实现具有签名冲突的类/接口
- 在java的枚举中,应该避免使用name字段或getName()这样的方法,避免kotlin中使用的问题
以上是关于kotlin与java互操作中的冲突的主要内容,如果未能解决你的问题,请参考以下文章
KotlinKotlin 与 Java 互操作 ③ ( Kotlin 中处理 Java 异常 | Java 中处理 Kotlin 异常 | @Throws 注解处理异常 | 函数类型互相操作 )
深入kotlin - 与Java互操作:kotlin调用java
KotlinKotlin 与 Java 互操作 ① ( 变量可空性 | Kotlin 类型映射 | Kotlin 访问私有属性 | Java 调用 Kotlin 函数 )