Kotlin的SAM转换

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的完美解决方案快点到来吧。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值