我该如何让开发者覆盖接口中的两个方法中的其中一个?
我在用Kotlin开发一个库,但需要实现Java与 Kotlin的互操作性。我的接口有一个接收Lambda表达式的方法。它被重载为两个版本:一个接收Kotlin的 lambda,一个接收Java的 Consumer。我该如何让实现者至少覆盖其中一个,以防止递归调用和栈溢出?
interface MyInterface {
// Java developers MUST override this method
fun process(callback: Consumer<MyClass>) {
process(callback::accept);
}
// Kotlin developers MUST override this method
@JvmSynthetic
fun process(callback: (MyClass) -> Unit) {
process(Consumer(callback::invoke));
}
}
解决方案
我认为要实现你想要的效果,唯一比较合理的办法,是定义一个仅在Java中使用的接口来扩展“真实的”接口。这种做法在 gidds 的一个注释中被提及。
例如:
import java.util.function.Consumer;
// The "real" interface
interface MyInterface {
fun process(callback: (MyClass) -> Unit)
}
// The Java-specific subtype of the "real" interface
interface MyJavaInterface : MyInterface {
fun process(callback: Consumer<in MyClass>)
override fun process(callback: (MyClass) -> Unit) {
process(Consumer(callback))
}
}
Kotlin代码将实现 MyInterface,并被迫实现该函数的 (MyClass) -> Unit 版本;而Java代码将实现 MyJavaInterface,并被迫实现 Consumer 版本的函数。并且由于 MyJavaInterface 是 MyInterface 的子类型,因此前者可以在后者能使用的任何地方使用。不过请注意,这并不能阻止 MyJavaInterface 的实现者覆盖 (MyClass) -> Unit 版本。若要防止这种情况,你需要把 MyJavaInterface 设为一个抽象类,并将该版本的函数声明为final。
否则你也可以尝试实现一个注解处理器,正如 这个注释 by rzwitserloot 所提及的那样。不过,这样就依赖于库的使用者实际使用你的注解处理器。此外,你还需要两份实现:一份给Kotlin,一份给Java。
也就是说,这个思路从一开始就可能有瑕疵吗?我会重新考虑只保留一个接口,并且只包含其中一个函数。表面上我会选择 (MyClass) -> Unit 版本,因为这是一个Kotlin库。从Java的角度来看,它将是一个像其它函数式接口一样的接口,因此可以用lambda表达式或方法引用来实现。然而,正如Louis Wasserman在注释中所指出的,Java代码将被迫返回 Unit.INSTANCE,这并不太符合人体工学。因此,既然你在设计时就把Java用户放在首位,最好只保留 Consumer 版本。
换句话说,最好只有:
import java.util.function.Consumer
interface MyInterface {
fun process(callback: Consumer<in MyClass>)
}
Kotlin代码仍然可以通过尾随lambda调用 process:
fun foo(arg: MyInterface) {
arg.process {
// do something...
}
}
如果你希望让Kotlin实现者像调用其他Kotlin函数类型一样调用回调(使用 callback(arg) 而不是 callback.accept(arg)),那么你可以定义一个运算符扩展函数:
operator fun <T> Consumer<T>.invoke(t: T) = accept(t)
(我不知道标准库,或任何第三方库,是否已经为标准Java函数类型提供了这样的运算符扩展函数。)