

Kotlin Malformed Anonymous Class Names in Interfaces - 找到编译器的 bug 是种怎样...
source link: https://www.liuwj.me/posts/kotlin-malformed-anonymous-class-names-in-interfaces/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Kotlin Malformed Anonymous Class Names in Interfaces - 找到编译器的 bug 是种怎样的体验?
本文来自我的知乎回答:找到编译器的bug是种怎样的体验? - 知乎
emmm…这个问题下面真的是大佬云集,萌新感到好忐忑…
前段时间在使用 Kotlin 开发一个 ORM 框架(广告慎入,Ktorm:专注于 Kotlin 的 ORM 框架),当时我的代码大概是这样的,定义了一个 Foo 接口,在这个接口里面写了个默认实现的 bar() 方法:
1
2
3
4
5
6
7
8
9
10
11
interface Foo {
fun bar() {
val obj = object : Any() { }
println(obj.javaClass.simpleName)
}
}
fun main(args: Array<String>) {
val foo = object : Foo { }
foo.bar()
}
怎么样,看起来是不是稳如狗?然而,这段代码在运行的时候,却喷了我一脸异常:
1
2
3
4
5
6
7
8
Exception in thread "main" java.lang.InternalError: Malformed class name
at java.lang.Class.getSimpleBinaryName(Class.java:1450)
at java.lang.Class.getSimpleName(Class.java:1309)
...
Caused by: java.lang.StringIndexOutOfBoundsException: String index out of range: -3
at java.lang.String.substring(String.java:1931)
at java.lang.Class.getSimpleBinaryName(Class.java:1448)
... 4 more
风中凌乱…我不就是想输出一下匿名对象的类名吗,这个 InternalError 是什么鬼…
惊讶之余,冷静下来好好理了理 Kotlin 生成 class 的规则,终于明白过来。
众所周知,在 Java 中,interface 里面是不能有方法实现的(Java 8 以前),然而,Kotlin 却可以直接在接口里面写实现方法。我们知道,Kotlin 最终也是要编译成 Java 字节码,既然 Java 本身都不支持这种操作,Kotlin 是怎么做到的呢?
反编译 Kotlin 生成的字节码就可以看到,在编译出来的 interface Foo 中,bar 方法仍然是 abstract 的,并没有实现。但是,Kotlin 另外生成了一个 Foo$DefaultImpls 类,在这个类里面有一个静态方法,这个方法的签名是:
1
public static void bar(Foo $this)
这个方法里面的字节码,就是我们的 bar() 方法的默认实现了。这样,当一个 Kotlin 的类实现了 Foo 接口时,编译器就会自动为我们插入一个 bar() 方法的实现,这个实现只是简单调用了 Foo$DefaultImpls 里面的静态方法:
1
2
3
4
@Override
public void bar() {
DefaultImpls.bar(this);
}
这就是 Kotlin 中接口默认方法的实现原理。
然而这跟前面的 bug 又有什么关系…
我们回过头来看刚刚出 bug 的代码,可以看到一个 object : Any() { },这应该会生成一个匿名内部类,看下编译结果,可以知道这个匿名内部类的名字是 Foo$bar$obj$1,这应该没什么特别的。
然后顺着异常栈去到 JDK 的 Class 类里面,看源码,可以看到报错的地方是这样的:
1
2
3
4
5
6
7
8
9
10
11
private String getSimpleBinaryName() {
Class<?> enclosingClass = getEnclosingClass();
if (enclosingClass == null) // top level class
return null;
// Otherwise, strip the enclosing class' name
try {
return getName().substring(enclosingClass.getName().length());
} catch (IndexOutOfBoundsException ex) {
throw new InternalError("Malformed class name", ex);
}
}
额,好像找到原因了…
回到前面提到的匿名内部类 Foo$bar$obj$1,因为 bar() 方法是在 Foo$DefaultImpls 中实现的,所以对这个匿名类获取 enclosingClass 毫无疑问就是 Foo$DefaultImpls 了,然后在 substring 的时候就 GG 了…
最后,根据我粗浅的理解,应该可以得出结论,这个 bug 的根源是 Kotlin 在编译这个匿名内部类的时候生成的名字有误,如果生成的名字是 Foo$DefaultImpls$bar$obj$1 的话,bug 就不会发生。带着这个疑惑,我去 Kotlin issue 上面找了找,果然已经有人提出过这个问题,然而这个 issue 至今都是 open 状态,并没有得到解决,难道是这个 bug 会牵扯到其他地方?有兴趣的同学可以去看一看:Names for anonymous classes in interfaces are malformed : KT-16727
最终,bug 的原因是找到了,那在 Kotlin 修复这个 bug 之前应该怎么办呢?我们当然只能想办法绕过了,比如避免在接口的默认实现方法中使用匿名内部类,lambda 也不行,因为 Kotlin 的 lambda 也会编译成匿名类…
BTW,说到编译器的 bug,之前在使用 Java 8 的 lambda 的时候也遇到过一个,当时还在知乎吐槽了一下,这里也贴个链接,仅作记录:此处的lambda为什么不能用方法引用表示 - 知乎
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK