背景
Kotlin在编译成class文件时,会给属性自动生成get/set方法,如果一个Kotlin类覆盖了Java父类的同名get/set方法,会导致编译异常。编译时会提醒存在相同的方法签名,导致编译失败。这篇文章简单介绍这种情况出现的原因、场景和解决方案。
一、编译失败的原因
1.1、Kotlin属性自动生成get/set
在Kotlin中,当你定义一个属性时,编译器会自动为你生成对应的getter和setter方法。而Kotlin本身可以直接使用对象.属性的方式获取/设置属性值:
kotlin
复制代码
class Person { var name: String = "John Doe" } val person = Person() person.name = "Jane Doe" // 调用setter println(person.name) // 调用getter
Kotlin自动生成的get/set是遵循驼峰命名规则的,所以如果通过Java调用就会如下:
java
复制代码
Person person = new Person("John Doe"); String name = person.getName(); // 获取属性 person.setName("Jane Doe"); // 设置属性
2.2、Java倾向于使用get/set定义属性
在Kotlin中,接口也是可以定义抽象属性,让最终实现接口的子类实现属性:
kotlin
复制代码
interface MyInterface { var prop: Int // 抽象的 val propertyWithImplementation: String get() = "foo" } class MyClass : MyInterface { override var prop: Int = 29 // 提供实现 }
但是在Java中,接口不能像类那样有实例字段。但是,接口可以有静态的不可变字段(也就是常量)。例如:
java
复制代码
public interface MyInterface { String MY_CONSTANT = "SomeValue"; }
在这个例子中,MY_CONSTANT
是一个常量,它的值不能被改变。
如果你想要在接口中定义一些属性,然后让实现这个接口的类去实现这些属性,只能使用抽象方法。例如:
java
复制代码
public interface MyInterface { void setMyValue(String value); String getMyValue(); }
2.3 当Kotlin通过属性实现Java的get/set方法时
当Kotlin类实现Java接口时,如果试图通过自动生成的get/set覆盖Java接口的get/set,方法签名就会重复导致编译失败:
kotlin
复制代码
class MyUser : UserDetails { var username: String? = null var password: String? = null override fun getAuthorities(): MutableCollection<out GrantedAuthority> { return mutableListOf() } override fun getPassword(): String? { return password } override fun getUsername(): String? { return username } override fun isAccountNonExpired(): Boolean { return true } override fun isAccountNonLocked(): Boolean { return true } override fun isCredentialsNonExpired(): Boolean { return true } override fun isEnabled(): Boolean { return true } }
- 这里编译就会产生签名重复的错误。
- 如果试图去除
override fun getPassword(): String?
函数和override fun getUsername(): String?
函数,编译会出现未实现抽象方法的错误。 - 如果尝试在
username
和password
属性上写override关键字,也会报错,因为Java接口根本没有抽象属性供子类覆盖。
二、出现的场景
这种编译错误一般出现在Kotlin试图直接通过属性自动生成get/set实现Java接口定义的get/set函数的时候,例如上面例子中,实现Spring Security的UserDetails
。
三、解决方案
3.1、修改属性名称
既然Kotlin是根据驼峰自动生成get/set,那么将属性的名称修改一下,使得生成的get/set和Java接口不一致就可以了:
kotlin
复制代码
class MyUser : UserDetails { var username_: String? = null var password_: String? = null override fun getAuthorities(): MutableCollection<out GrantedAuthority> { return mutableListOf() } override fun getPassword(): String? { return password_ } override fun getUsername(): String? { return username_ } override fun isAccountNonExpired(): Boolean { return true } override fun isAccountNonLocked(): Boolean { return true } override fun isCredentialsNonExpired(): Boolean { return true } override fun isEnabled(): Boolean { return true } }
3.1.1、优点:
- 简单修改属性名称就可以解决问题。
3.1.2、缺点:
- 虽然能够实现获取设置属性的功能,但是属性名称和接口想要表达的get/set命名不统一(不符合驼峰),违反直觉约定。
- 在上面的例子中,通过外部数据如SQL查询,获取结果可能无法正确映射set到对应属性。
3.2、修改get/set编译后名称
在Kotlin中有一个注解,可以让我们自定义get/set方法的名称:
kotlin
复制代码
class MyUser : UserDetails { var username: String? = null @JvmName("getUsernameForKt") get var password: String? = null @JvmName("getPasswordForKt") get override fun getAuthorities(): MutableCollection<out GrantedAuthority> { return mutableListOf() } override fun getPassword(): String? { return password } override fun getUsername(): String? { return username } override fun isAccountNonExpired(): Boolean { return true } override fun isAccountNonLocked(): Boolean { return true } override fun isCredentialsNonExpired(): Boolean { return true } override fun isEnabled(): Boolean { return true } }
3.2.1、优点
- 保持属性名称不变,符合直觉约定。
- 由于1的关系,各种框架在映射时不会出现属性对应不上的问题。
3.2.2、缺点
- 在某些通过get方法获取类属性的框架中,
@JvmName
生成的函数和继承的get函数会被认为是两个属性。如Jackson序列化的Json会出现两个值一样的字段,这点需要注意。