如何在 Kotlin 覆写 static 方法

Posted by rarnu on 03-02,2021

初看标题你是不是会有个很大的疑问甚至想骂人? 是的,当我遇到这个问题时,我也很想骂人,因为 static 方法原本就不参与继承,又何来覆写一说呢?

但是在 Kotlin 环境下,这种情况确实会发生,我来举个栗子,例如在开发 swing 的主题时,对于每一种 UI,都需要实现以下静态方法:

public static ComponentUI createUI(JComponent c) {
    return new MyUI();
}

并且 swing 将以 反射 机制调用该方法,也就是说,这个方法的命名只能是 createUI,以及它的参数类型只能是 JComponent

看到这里,你的反应是不是,在 Kotlin 里也很简单啊,写以下代码就可以了:

companion object {
    fun createUI(c: JComponent): ComponentUI = MyUI()
}

其实这个时候,你已经犯了一个很大的错误了,因为 Kotlin 的 companion,本质是伴生对象,而不是静态对象,所以在编译后,根本无法得到一个签名为 (Ljavax/swing/JComponent;)Ljavax/swing/plaf/ComponentUI; 的静态方法,所以 swing 并不会找到并调用它。

于是这个时候你又要说了,加上 @JvmStatic 注解不就好了,这样就能得到 Java 的静态方法了。是的,这个想法没错,但是坏就坏在这个注解上,因为加入了这个注解,才会产生我们标题上所说的 覆写 static 方法 的现象,并且提示为不允许覆写 static 方法(这是对的,static 方法本来就不能被覆写)。由于该方法的调用机制是反射,因此也不能采用 @JvmName 注解来修改它的编译名称。

不卖关子了,我直接说解决方案,解决起来非常简单,加一句代码:

companion object {
    @Suppress("ACCIDENTAL_OVERRIDE")
    @JvmStatic
    fun createUI(c: JComponent): ComponentUI = MyUI()
}

好了,那下面来简单说一下这个问题的由来,其实根本问题出在 Kotlin 对 继承 的判定上,如果都是 Kotlin 代码,那么继承时遇到 companion,无非也就是各自生成一个伴生对象,谁也不影响谁,所以纯 Kotlin 的情况下,使用 companion 可以实现与 Java 一致的带有静态方法的继承。

而 Kotlin 类继承自 Java 类时,这就有所不同了,Java 的静态方法会被带入 Kotlin 的子类中,这本来也不是什么问题,但是当你试图拥有一个与父类静态方法同名且同参数类型的方法时,bug 就出现了,提示为 accidental override。也就是说,kotlin 其实会尝试将静态方法覆写,最后发现覆写不了,然后报个错出来。

回过头来再看这个注解,@Suppress("ACCIDENTAL_OVERRIDE"),其本质就是禁止这个覆写的行为,这样一来,@JvmStatic 就可以正确生成与 Java 一样的静态方法,而不是尝试去覆写父类的静态方法。

最后再说一句,关于这个 @Suppress 的注解怎么来的,你可以查阅官方文档,或者是 YT 的记录,基本上都查得到,但是有一个更简单的办法,就是看异常信息。在 Kotlin 代码被编译时,会有出错信息出现,如本文中的 accidental override,这个关键字将会被放在异常信息的开头,你只需要记下这个词,并且将中间的空格换成下划线,写到 @Suppress 注解里就可以了。当然前提是你能确认这个异常是由于未 阻止 特定操作而引起的。

同时,也可以看看官方对这个问题的记载,点击左下角 阅读原文 就能看到了。

https://youtrack.jetbrains.com/issue/KT-12993