本文来自我的知乎回答:找到编译器的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为什么不能用方法引用表示 - 知乎

以上