Java中的SAM
Java8之后,我们将只有单一方法的接口称为SAM(Single Abstract Method)接口,Java8通过Lambda可以大大简化对于SAM接口的调用。没有引入Lambda之前,我们是这样调用SamType的:
interface JavaInterface {
String doSomething(Item item);
}
String delegateWork(JavaInterface f) {
return f.doSomething(item);
}
void doWork() {
delegateWork(new JavaInterface() {
@Override
public String doSomething(Item item) {
return "Item = " + item;
}
});
}
我们需要实现一个JavaInterface的匿名内部类对象,即使只有一个方法,也要规规矩矩地对方法进行声明。但在Java8之后,我们可以使用Lambda简化对SamType的调用进行简化:
@FunctionalInterface
interface JavaInterface {
String doSomething(Item item);
}
String delegateWork(JavaInterface f) {
return f.doSomething(item);
}
void doWork() {
delegateWork(item -> "Item = " + item);
}
我们将上述借助Lambda对SamType调用的优化称为SAM转换(Single Abstract Method Conversions)
Kotlin兼容Java中的SAM转换
我们在Kotlin中使用在Java中定义的delegateWork方法时,SAM转换同样有效:
//java:定义delegateWork
String delegateWork(JavaInterface f) {
return f.doSomething(item);
}
//kt:调用delegateWork
delegateWork { "Print $it" }
Kotlin将Java的SamType翻译成了lambda,因此在kotlin的同名方法实际变成了一个高阶函数:
String delegateWork(JavaInterface f)
转换为👇
fun delegateWork(f:(ITEM) -> String)
SAM构造方法
当我们在kotlin中定义delegateWork时:
//kt:定义delegateWork
fun delegateWork(f: JavaInterface): String {
return f.doSomething(item)
}
//kt:
//Error: Kotlin: Type mismatch: inferred type is () -> String but JavaInterface was expected
delegateWork { "Print $it" }
此时delegateWork在kotlin中有明确定义,并不是一个高阶函数,所以此时只能用以下两种方式调用:
//方案一:匿名类对象
delegateWork( object : JavaInterface {
override fun doSomething(item: Item) {
"Print $item"
}
})
//方案二:SAM构造方法(推荐)
delegateWork( JavaInterface { "Print $it" } )
此时Kotlin也是有优化方案的:用SAM构造方法替代传统的匿名类方式创建对象。除了书写更加简单以外,性能方面也有优势,相对于匿名类对象每次调用都会创建一个新对象,SAM构造方法会保持一个静态对象的引用:
delegateWork( JavaInterface { "Print $it" } )
等价于👇
static JavaInterface INSTANCE = new JavaInterface() {
@NotNull
public final String doSomething(Item it) {
return "Print " + it.toString()
}
}
delegateWork(INSTANCE); //复用静态变量
但是需要注意的是当SAM中捕捉了外部变量时,仍然会每次创建新对象:
lateinit var str : String
delegateWork( JavaInterface {
"Print $it $str" // -> 使用外部变量str,JavaInterface每次创建新对象
})
等价于 👇
String ss = ""
delegateWork(new JavaInterface() {
@NotNull
public final String doSomething(Item it) {
return "Print " + it.toString() + str
}
});
Kotlin定义SAM接口
当我们在Kotlin中定义SAM接口时
interface KotlinInterface {
fun doSomething(item: Item): String
}
class KotlinComponent(private val item: Item = Item()) {
fun delegateWork(f: KotlinInterface): String {
return f.doSomething(item)
}
}
此时,上面无论那种方法调用都会报错
delegateWork { "Print $it" }
//Error: Kotlin: Type mismatch: inferred type is () -> String but KotlinInterface was expected
delegateWork(KotlinInterface { "Print $it" })
//Error: Kotlin: Interface KotlinInterface does not have constructors
只能用匿名类对象的方式进行调用:
delegateWork(object : KotlinInterface {
override fun doSomething(item: Item): String {
return "Print $item"
}
})
可见,Kotlin中的SamType无法转化为lambda,因为kotlin鼓励开发者多尝试用函数式编程的思想替换面向对象的思维方式,所以推荐使用函数类型替代SamType。
SAM转换的替代方案
虽然官方推荐用函数类型替代SamType,但是仍然有很多开发者认为SamType的语义更加明确,希望KClass的SAM转换能在未来得到支持:https://youtrack.jetbrains.com/issue/KT-7770。具说在kotlin1.4中这一问题会得到解决。但目前只能通过一些替代方案实现SAM转换
方案1:Java定义SamType
在Java中定义Sam接口,这样就可以用SAM构造方法的方式在kotlin中调用了。只是这会导致额外维护一份java文件
方案2:typealias
typealias KotlinFunctionAlias = (Item) -> String
fun delegateAliasWork(f: KotlinFunctionAlias): String {
return f.invoke(item)
}
这样,我们可以使用lambda的方式进行调用
delegateAliasWork { "Print $it" }
但是当有多个同类型的typealias时,是无法保证类型安全的:
//KotlinFunctionAlias2于KotlinFunctionAlias的实际类型都是(Item) -> String
typealias KotlinFunctionAlias2 = (Item) -> String
val f : KotlinFunctionAlias2 = TODO()
delegateAliasWork(f) //KotlinFunctionAlias的参数类型也可以接受KotlinFunctionAlias2
方案3:使用KClass封装
//Class封装SamType
class KotlinInterface(val doSomething: (Item) -> String)
//方法
fun delegateInlineWork(f: KotlinInterface): String {
return f.doSomething.invoke(item)
}
//调用
delegateWork(KotlinInterface { "Print $it" })
用KClass替代SamType的Interface,函数成员封装Interface方法的实现。但是这种调用方式会创建两个对象:函数成员对象以及class对象。当然我们可以通过inline class的方式进行优化,避免class对象的创建,变成下面这样:
//class封装SamType
inline class KotlinInlineInterface(val doSomething: (Item) -> String)
//方法定义
fun delegateInlineWork(f: KotlinInlineInterface): String {
return f.doSomething.invoke(item)
}
//调用
delegateInlineWork(KotlinInlineInterface { "Print $it" })
但是inline class目前还处于实验状态,经实际测试在某些场景下存在bug,不推荐大家大量使用。
结论
首先,态度鲜明地推荐大家在Kotlin中尽量使用函数类型替代SamType,当实在需要使用SamType时,可以选择上述三种替代方案,但通过分析我们知道这三种方案各有缺陷,最靠谱的还是期盼1.4的完美解决方案快点到来吧。