This post is a copy from previous posts on Medium (initial, follow-up) But since I'm planning on deleting my Medium account I moved them here.
Kotlin是一种很棒的编程语言。 经过大约12年的Java编程工作,与Kotlin一起工作了多年之后,感觉就像戴上眼镜一样:有太多的爱。
但是,就像每一个恋爱关系一样,您在生活的晚些时候只会发现一些怪癖。 在将越来越多的Java代码迁移到Kotlin代码之后,我注意到了一些比较奇怪且坦率的令人讨厌的地方。
就是这样科特林处理功能接口。
Java 7: a blast from the past
让我们回到没有lambda的世界。 太冗长了!
interface JavaInterface {String doSomething(Item item);
}String delegateWork(JavaInterface f) {return f.doSomething(item);
}void doWork() {delegateWork(new JavaInterface() {@Overridepublic String doSomething(Item item) {return "Item = " + item;}});
}
Java 8: Lambdas to the rescue!
最终,Java 8为我们提供了Lambdas,我们可以摆脱很多代码,专注于重要的事情。 同样,我们也不必为每个简单的函数编写自己的函数接口,而只需使用oracle提供的某些函数即可,例如:java.util.function.Function<T, R>
@FunctionalInterface
interface JavaInterface {String doSomething(Item item);
}String delegateWork(JavaInterface f) {return f.doSomething(item);
}String delegateOtherWork(Function<Item, String> f) {return f.apply(item);
}void doWork() {delegateWork(item -> "Item = " + item);delegateOtherWork(item -> "Item = " + item);
}
一切都很好,直到您意识到即使您现在拥有函数类型,它们仍不是该语言的一等公民。 要证明吗? 猜猜Java中必须引入多少个“函数类型”? 一? 三? 五?
43!
Don't believe me, see for yourself: https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html
And if that's not enough for you, add jOOL to the mix and you have access to 35 more: https://github.com/jOOQ/jOOL/tree/master/jOOL/src/main/java/org/jooq/lambda/function
Because who wouldn't love coming across a method signature that looks like this:
Function5<String, String, String, String, String, Tuple3<String, String, String>> higherOrder(Function12<String, Integer, String, Object, Object, Object, BiFunction<String, Integer, String>, String, Integer, Long, String, Double, Optional<Tuple2<String, String>>>)
😜旁注:jOOL实际上是一个非常简洁的库,值得一试。
Kotlin help us!
现在,将Kotlin添加到混合中。 在科特林,职能是一等公民。 因此,无需记住数十种稍有不同的功能类型。 您只需要记住Kotlin的函数类型语法:
(Parameter1Type, Parameter2Type, ParameterNType) -> ReturnType
就是这样,仅此而已。
Trouble in paradise
好吧,我们为什么在这里,怎么了?
如前所述,随着我将越来越多的代码从Java迁移到Kotlin。 使用自定义功能接口时遇到一些问题。 因为有时候您想要那种额外的描述性。
回到我们的Java 8示例
@FunctionalInterface
interface JavaInterface {String doSomething(Item item);
}class JavaComponent {private Item item = new Item();String delegateWork(JavaInterface f) {return f.doSomething(item);}String delegateOtherWork(Function<Item, String> f) {return f.apply(item);}
}
现在让我们从Kotlin代码中使用它
delegateWork { "Print $it" }
delegateOtherWork { "Print $it" }
很好,这很棒,正好符合我们的期望! 好吧,现在让我们迁移一下Java组件上科特林。 请注意,我们已经更改了java.util.function.Function<Item, String>到Kotlin函数类型(Item) -> String
class KotlinComponent(private val item: Item = Item()) {fun delegateWork(f: JavaInterface): String {return f.doSomething(item)}fun delegateOtherWork(f: (Item) -> String): String {return f.invoke(item)}
}
让我们看看使用Java代码中的这些高阶函数会发生什么。
delegateWork(item -> "Print: " + item);
delegateOtherWork(item -> "Print: " + item);
没有什么与众不同的,我们可以对两种方法使用相同的lambda。 让我们看看当我们完成Kotlin的预期时会发生什么:
delegateWork { "Print $it" }Error: Kotlin: Type mismatch: inferred type is () -> String but JavaInterface was expected
What happened? It seems the compiler can't figure out that the signature of the lambda is the same as the functional interface method. https://kotlinlang.org/docs/reference/java-interop.html#sam-conversions
因此,我们必须明确地说出我们的期望:
delegateWork(JavaInterface { "Print $it" })
我认为这很令人失望,但还算不错。 现在让我们看看将接口迁移到Kotlin时会发生什么:
interface KotlinInterface {fun doSomething(item: Item): String
}class KotlinComponent(private val item: Item = Item()) {fun delegateWork(f: KotlinInterface): String {return f.doSomething(item)}fun delegateOtherWork(f: (Item) -> String): String {return f.invoke(item)}
}
当我们使用Kotlin组件从Java类开始,正如预期的一样,lambda保持完全相同。 如果我们从Kotlin代码中使用它,该怎么办:
delegateWork { "Print $it" }Error: Kotlin: Type mismatch: inferred type is () -> String but KotlinInterface was expected
看来SAM转换再次失败。 现在,如果我们像以前一样明确提及接口,该怎么办?
delegateWork(KotlinInterface { "Print $it" })Error: Kotlin: Interface KotlinInterface does not have constructors
这也没有帮助。 我们需要创建一个匿名对象以使其工作:
delegateWork(object : KotlinInterface {override fun doSomething(item: Item): String {return "Print $item"}
})
Yikes! This feels like working with Java 7 all over again. Sadly this is because Kotlin doesn't yet support SAM conversion for Kotlin interfaces so we have to create this anonymous object. See also:
https://youtrack.jetbrains.com/issue/KT-7770
https://stackoverflow.com/a/43737962/611032
Alias time!
那么,如何避免使用这些冗长的匿名对象,而仍然为该函数使用自定义名称? 我们使用类型别名:
/**
* Very helpful comment.
*/
typealias KotlinFunctionAlias = (Item) -> Stringfun delegateAliasWork(f: KotlinFunctionAlias): String {return f.invoke(item)
}
因此,现在我们可以按期望的方式传递lambda了,我们仍然可以从函数的自定义名称中受益。
delegateAliasWork { "Print $it" }
这样一切就好了,案件结案了,该回家了。 不幸的是不完全是。
Lost in translation
类型别名的一个小问题是,虽然您可以命名函数类型,但不能命名方法名称:
val iface: JavaInterface = JavaInterface { "Print $it" }
iface.doSomething(item)val alias: KotlinFunctionalAlias = { item -> "Print $item" }
alias.invoke(item)
alias(item)
为类型别名和变量选择好名字可以缓解此问题。 幸运的是,我们的开发人员擅长命名事物things
Type safety
更大的问题是,尽管类型别名为我们提供了不同的名称,但它们并不是真正的不同类型,因此我们实际上并不是安全的类型。
让我们来看一个Java示例,其中的两个功能接口具有相同的方法签名。
JavaInterface1 f1 = item -> "Print " + item;
JavaInterface2 f2 = item -> "Print " + item;
f1 = f2;Error: java: incompatible types: JavaInterface2 cannot be converted to JavaInterface1
这就是我们期望的,我们不想在这里混合苹果和橙子。
如果我们使用Kotlin类型别名做同样的事情,会发生什么? (我想你知道我要去哪里了)
var f1: KotlinFunctionAlias1 = { item -> "Print $item" }
var f2: KotlinFunctionAlias2 = { item -> "Print $item" }
var f3: (Item) -> String = { item -> "Print $item" }
f1 = f2
f2 = f3
f1 = f3
这样做很好,编译器不会抱怨,因为就像我提到的那样,它们实际上并不是不同的类型。 它们都是:(Item) -> String
Solutions
因此,让我们快速回顾一下解决Kotlin接口缺少的SAM转换以及Kotlin接口的优点和缺点的不同方法。
Leave functional interfaces as Java interfaces
+良好的Java互操作性 +支持自定义方法名称 +类型安全
-需要给Kotlin lambda加上接口名称前缀 -需要额外的括号 -需要维护Java代码
Use a type alias for Kotlin function types
+良好的Java互操作性 +易于使用
-不安全输入 -没有自定义方法名称
Use inline classes
我们尚未讨论的另一种选择是使用实验性Kotlin内联类。 您可以使用内联类“包装” Kotlin函数。
inline class KotlinInlineInterface(val doSomething: (Item) -> String)fun delegateInlineWork(f: KotlinInlineInterface): String {return f.doSomething.invoke(item)
}delegateInlineWork(KotlinInlineInterface { "Print $it" })
Even though this works, I don't thinks it's an appropriate way of using inline classes. Also Java interoperability isn't currently supported: https://kotlinlang.org/docs/reference/inline-classes.html#mangling
Always use Kotlin function types
是的,您可以使用(ParamT) -> ReturnT类型无处不在。 通常这就足够了,但是随着您的应用程序的增长,它可能会变得更难阅读和维护,并且更容易出错。
Live with anonymous objects
当然,如果您不介意,则可以只使用匿名对象,希望有一天Kotlin将支持完整的SAM转换,并利用出色的IDE集成将您的匿名对象迁移到lambdas。
¯\(ツ)/¯
Jetbrains Feedback
There has been a short discussion on Reddit: https://www.reddit.com/r/Kotlin/comments/bipj0q/functional_interfaces_selfloathing_in_kotlin/
从那时起,我得到了罗马·伊里扎洛夫(Roman Elizarov)的回应
Roman Elizarov@relizarov@ranilch @jetbrains youtrack.jetbrains.com/issue/KT-1112919:49 PM - 29 Apr 20190 2
我尝试了提到的Kotlin编译器选项:
// Gradle Kotlin DSL
tasks.withType<KotlinCompile> {kotlinOptions.freeCompilerArgs += "-XXLanguage:+NewInference"
}
// Gradle Groovy DSL
compileKotlin {kotlinOptions {freeCompilerArgs += "-XXLanguage:+NewInference"}
}
If you're more into other build systems, refer to Kotlin documentation (Maven / Ant) to see how to pass Kotlin compiler arguments.
Problem solved?
首先让我们看看在Kotlin代码中使用Kotlin功能接口时会发生什么:
fun delegateWork(f: KotlinInterface): String {return f.doSomething(item)
}delegateWork { item -> "Print: $item" }Error: Type mismatch: inferred type is (Nothing) -> TypeVariable(_L) but KotlinInterface was expected
显式指定接口呢?
delegateWork(KotlinInterface { item -> "Print $item" }Error: Interface KotlinInterface does not have constructors
mm! 我们仍然需要一个匿名对象。
怎么样使用Kotlin中的Java功能接口码?
fun javaInterface(f: JavaInterface) {val res = f.doSomething(item)output(res)
}javaInterface { item -> "Print: $item" }
最后:正是我们所期望的。 一切都很好,啤酒当之无愧!
Patience young Jedi
如果您观察的话,会在构建过程中看到以下内容:
w: ATTENTION!
This build uses unsafe internal compiler arguments:
-XXLanguage:+NewInferenceThis mode is not recommended for production use,
as no stability/compatibility guarantees are given on
compiler or generated code. Use it at your own risk!
那是什么意思呢? 这意味着它在这里所说的内容:使用起来还不是很安全。 但是了解到JetBrains正朝着这个方向努力时,我建议我们暂时按照以下方式进行操作(从最有利到最不利)
- 将功能接口保留为Java代码为Kotlin函数类型使用类型别名(如果您可以将苹果和橙子混合使用)与匿名对象一起生活
谢谢阅读。 一如既往,我乐于接受批评和反馈。