Kotlin 2.1.0 入门教程(十八)函数式接口
函数式接口
只有一个抽象成员函数的接口被称为函数式接口,或单抽象方法(SAM
)接口。函数式接口可以有多个非抽象成员函数,但只能有一个抽象成员函数。
声明函数式接口,需使用 fun
修饰符。
fun interface KRunnable {
fun invoke()
}
fun interface Foo {
fun accept(i: Int): Boolean
}
class Goo : Foo {
override fun accept(i: Int): Boolean {
println("${i % 2 == 0}")
return i % 2 == 0
}
}
fun main() {
val goo = Goo()
goo.accept(1) // false
}
SAM
转换
对于函数式接口,你可以使用 SAM
转换。通过使用 Lambda
表达式,SAM
转换有助于使你的代码更加简洁易读。
你无需手动创建一个实现函数式接口的类,而是可以使用 Lambda
表达式。借助 SAM
转换,Kotlin
能够将任何签名与接口唯一方法的签名相匹配的 Lambda
表达式转换为动态实例化接口实现的代码。
例如,考虑以下函数式接口:
fun interface IntPredicate {
fun accept(i: Int): Boolean
}
如果你不使用 SAM
转换,就需要编写如下代码:
// 创建一个类的实例。
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}
利用 SAM
转换,你可以编写如下等效代码来替代:
// 使用 Lambda 表达式创建实例。
val isEven = IntPredicate { it % 2 == 0 }
一个简短的 Lambda
表达式就取代了所有不必要的代码。
fun interface Foo {
fun accept(i: Int): Boolean
fun func(bool: Boolean) = println(bool)
}
fun main() {
val foo = Foo { i -> i % 2 == 0 }
foo.func(foo.accept(1)) // false
}
从带构造函数的接口迁移到函数式接口
从 1.6.20
版本开始,Kotlin
支持对函数式接口的构造函数使用可调用引用,这为从带构造函数的接口迁移到函数式接口提供了一种源代码兼容的方式。请看以下代码:
interface Printer {
fun print()
}
fun Printer(block: () -> Unit): Printer = object : Printer {
override fun print() = block()
}
对函数式接口的构造函数使用可调用引用后,这段代码可以直接替换为一个函数式接口声明:
fun interface Printer {
fun print()
}
其构造函数会被隐式创建,并且任何使用 ::Printer
函数引用的代码都能正常编译。例如:
documentsStorage.addPrinter(::Printer)
为保持二进制兼容性,可使用 @Deprecated
注解(级别设为 DeprecationLevel.HIDDEN
)标记旧的 Printer
函数:
@Deprecated(message = "Your message about the deprecation", level = DeprecationLevel.HIDDEN)
fun Printer(...) {...}
迁移过程
在 Kotlin
中,从带有构造函数的接口迁移到函数式接口可以利用 1.6.20
及以后版本对函数式接口的构造函数的可调用引用的支持。
初始代码:带有构造函数的接口
假设我们有一个用于处理文档打印的系统,一开始定义了一个普通接口 Printer
,并提供了一个工厂函数来创建该接口的实例。
// 定义普通接口。
interface Printer {
fun print()
}
// 工厂函数,用于创建 Printer 接口的实例。
fun Printer(block: () -> Unit): Printer = object : Printer {
override fun print() = block()
}
class DocumentsStorage {
private val printers = mutableListOf<Printer>()
fun addPrinter(printer: Printer) {
printers.add(printer)
}
fun printAllDocuments() {
printers.forEach { it.print() }
}
}
fun main() {
val documentsStorage = DocumentsStorage()
// 使用工厂函数创建 Printer 实例。
val printer = Printer {
println("Printing document...")
}
documentsStorage.addPrinter(printer)
documentsStorage.printAllDocuments()
}
迁移后的代码:函数式接口
从 1.6.20
开始,我们可以将其迁移为函数式接口,利用函数式接口的隐式构造函数来简化代码。
::Printer
:这是对函数式接口 Printer
的隐式构造函数的可调用引用。
// 定义函数式接口。
fun interface Printer {
fun print()
}
class DocumentsStorage {
private val printers = mutableListOf<Printer>()
fun addPrinter(printer: Printer) {
printers.add(printer)
}
fun printAllDocuments() {
printers.forEach { it.print() }
}
}
fun main() {
val documentsStorage = DocumentsStorage()
// 使用可调用引用。
fun customPrint() {
println("Custom printing document...")
}
documentsStorage.addPrinter(::customPrint)
// 使用可调用引用。
documentsStorage.addPrinter({
println("Custom printing document...")
})
// 使用可调用引用。
documentsStorage.addPrinter(::Printer {
println("Printing document...")
})
documentsStorage.printAllDocuments()
}
保持二进制兼容性
为了确保旧代码仍然可以正常工作,并且不影响使用旧接口的二进制兼容性,我们可以使用 @Deprecated
注解标记旧的工厂函数。
// 定义函数式接口。
fun interface Printer {
fun print()
}
// 标记旧的工厂函数为弃用。
@Deprecated(message = "Use the functional interface constructor directly.", level = DeprecationLevel.HIDDEN)
fun Printer(block: () -> Unit): Printer = object : Printer {
override fun print() = block()
}
通过这种方式,我们既实现了代码的迁移和简化,又保证了旧代码的兼容性。随着时间推移,开发者可以逐渐替换掉旧的工厂函数调用,使用更简洁的函数式接口的构造方式。
函数式接口与类型别名对比
你也可以简单地使用函数类型的类型别名来重写上述代码:
typealias IntPredicate = (i: Int) -> Boolean
val isEven: IntPredicate = { it % 2 == 0 }
fun main() {
println("Is 7 even? - ${isEven(7)}")
}
然而,函数式接口和类型别名有着不同的用途。类型别名只是现有类型的名称 —— 它们不会创建新的类型,而函数式接口则会创建新类型。你可以为特定的函数式接口提供扩展,而这些扩展对普通函数或其类型别名并不适用。
类型别名只能有一个成员,而函数式接口可以有多个非抽象成员函数和一个抽象成员函数。函数式接口还可以实现和继承其他接口。
函数式接口比类型别名更灵活,功能也更强大,但它们在语法和运行时可能成本更高,因为可能需要转换为特定的接口。当你在代码中选择使用哪一种时,要考虑自己的需求:
-
如果你的
API
需要接受具有特定参数和返回类型的函数(任意函数),那么使用简单的函数类型,或者定义一个类型别名,为相应的函数类型取一个更简短的名称。 -
如果你的
API
接受的是比函数更复杂的实体 —— 例如,它有复杂的约定和 / 或其上的操作无法用函数类型的签名来表达 —— 那么为其声明一个单独的函数式接口。