背景
看了 让你迷惑的 Kotlin 代码 有感, 自己前些天也遇到了一些令人困惑的 Kotlin 代码/语法糖实现, 在这里分享一下
上代码
下面反编译的代码都有修改, 优化了可读性
代码是一个简单的 类委托 + interface default method
object App {
@JvmStatic
fun main(args: Array<String>) {
val origin = object : Service {
override fun getName(): String = "origin"
}
val proxy = object : Service by origin {
override fun getName(): String = "proxy"
}
proxy.printName()
}
}
interface Service {
fun getName(): String
fun printName() {
println(getName())
}
}
复制代码
大家可以先想一下这段代码会输出什么内容
答案是 “origin”, 这里可能会和预期有些不符
但是如果我们给printName() 添加一个 @JvmDefault注释, 答案就会是 “proxy”
interface Service {
fun getName(): String
@JvmDefault
fun printName() {
println(getName())
}
}
复制代码
原理
这里涉及到 以下知识点:
- kotlin interface default method 默认实现 和 加了@JvmDefault下的实现
- 类委托的原理
interface default method 的原理
默认情况下
通过反编译得到的Java代码, 我们可以看到生成了一个 Service.DefaultImpls
类, 里面有我们定义的 Kotlin 中的默认方法 printName(Service)
, 该方法需要传入一个 Service
对象(关键点1).
Service
的子类对默认方法 printName()
的调用, 其实是调用了 Service.DefaultImpls
中的方法, 默认传入了 this
(关键点2)
new Service() {
@NotNull
public String getName() {
return "origin";
}
public void printName() {
Service.DefaultImpls.printName(this);
}
};
public interface Service {
@NotNull
String getName();
void printName();
public static final class DefaultImpls {
public static void printName(@NotNull Service $this) {
String var1 = $this.getName();
boolean var2 = false;
System.out.println(var1);
}
}
}
复制代码
添加 @JvmDefault 注解后
需要添加 gradle 配置
compileKotlin {
kotlinOptions {
// or freeCompilerArgs = ['-Xjvm-default=compatibility']
freeCompilerArgs = ['-Xjvm-default=enable']
jvmTarget = '1.8'
}
}
复制代码
这里利用了 java 8 引入的 default
关键字, 使用了默认方法, 所以不会再生成其他对象了
new Service() {
@NotNull
public String getName() {
return "origin";
}
}
public interface Service {
@NotNull
String getName();
@JvmDefault
default void printName() {
String var1 = this.getName();
boolean var2 = false;
System.out.println(var1);
}
}
复制代码
类委托的原理
我们来看反编译之后的代码
Service proxy = new Service() {
// $FF: synthetic field
private final Service delegate = origin;
@NotNull
public String getName() {
return "proxy";
}
public void printName() {
this.delegate.printName();
}
};
复制代码
可以看到, 未重写的 printName()
方法, 内部调用了 delegate.printName()
(关键点3)
到现在结论就很明显了:
-
在未使用注解时:
proxy.printName()
->origin.printName()
->Service.DefaultImpls.printName(origin)
->origin.getName()
-> 输出 “origin” -
在使用
@JvmDefault
注解时:proxy.printName()
->proxy.getName()
-> 输出 “proxy”
如何避免
-
添加
@JvmDefault
注解, 这就不用说了 -
传入指定对象; 既然
Service.printName()
在调用Service.DefaultImpls.printName()
时会自动给我们传入当前对象, 我们只要给Service.printName()
添加一个Service
参数, 传入指定的对象就行了, 代码如下:interface Service { fun getName(): String fun printName(service: Service = this) { println(service.getName()) } } 复制代码
不过这种方式的 Java 实现可能会和你想象中的有所不同, 如果有兴趣的话, 可以自己去反编译看看