kotlin函数式接口SAM实际使用的坑

整理了自己在实际开发中关于kotlin的学习和思考:深入学习Kotlin,感兴趣的可以查看,后续会不断地更新。

函数式接口

函数式接口,英文名称(Functional (SAM) interfaces),全称是 Single Abstract Method (SAM) interface。在 Kotlin 中,仅具有一个抽象方法的接口称为函数式接口或单一抽象方法(SAM)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。比如下面的接口:

kotlin 复制代码
fun interface IServiceInterface {
   fun getService()
}

很多人看到这会觉得这个不是和普通的接口一样吗?是的,我开始的时候,也是这么认为,这不都一样的吗?但是你仔细看,就可以看到接口定义的时候,多了一个fun关键字。是的,函数式接口在定义的时候,多一个了fun关键字修饰,如果没有,那这个接口就是一个普通的接口。

作用

那么函数式接口有什么作用?其实最大的作用就是:可以做 ASM 转化,即把接口实现转为lambda实现

我们知道,在 kotlin 中,对于 Java 定义的SAM接口,会默认当作函数式接口。比如常用的设置Viewclick事件,我们正常可以类似 Java 的写法:

kotlin 复制代码
    view.setOnClickListener(object : OnClickListener {
        override fun onClick(v: View?) {
            TODO("Not yet implemented")
        }

    })

使用lambda实现的方式,如下面:

kotlin 复制代码
    view.setOnClickListener {
        TODO("Not yet implemented")
    }

可以看到,直接使用lambda的方式实现这个接口,写起非常的方便。

这是针对 Java 接口默认的操做。但是对于 kotlin 中定义的接口,默认是没有这种写法的,即使这个接口是一个SAM接口,比如下面这个接口:

kotlin 复制代码
interface IServiceInterface {
   fun getService()
}

他在实现的时候,就只能通过object创建匿名对象的方式实现调用

kotlin 复制代码
    setIServiceInterface(object : IServiceInterface {
        override fun getService() {
            print("定义的接口")
        }

    })

那如果要实现 lambda 的调用方式,就需要定义函数式接口,也就是:

kotlin 复制代码
fun interface IServiceInterface {
   fun getService()
}

这样的话,就能够实现

kotlin 复制代码
    setIServiceInterface{
        print("定义的接口")
    }

Android 源码中也定义了好多类似的接口,比如我们常用的Observer接口

kotlin 复制代码
fun interface Observer<T> {

    /**
     * Called when the data is changed is changed to [value].
     */
    fun onChanged(value: T)
}

实际使用中的坑

函数式接口在使用的时候确实很方便,但是使用不当或者不理解背后的原理,很可能会埋下坑,觉得逻辑很奇怪。

举个不太恰当的例子(仅仅是用来说明问题):

kotlin 复制代码
    // ViewModel请求数据,假设返回的值为:七郎
    viewModel.requestData()
    findViewById<ViewGroup>(R.id.tv1).apply {
         setOnClickListener {
            // 每次点击,就注册观察者,接收数据
            viewModel?.liveData?.observe(this@MainActivity) {
                // 打印获取到的数据,假设就是就是上面的:七郎
                Log.i("MainActivity", "return result is ${it}")
            }
        }
    }

上面的例子中,先是 ViewModel获取数据,然后每次点击文字控件tv1就注册一个观察者,监听数据回调。正常按照我的理解,每点击一次打印一次日志return result is 七郎。因为在点击之前已经加载了数据,按照 LiveData 的特性,后面注册的观察者,都会接收到一次上次的数据。但是实际上整个日志就打印了一次。

那这是为什么呢?我们反编译下 apk 的代码:

java 复制代码
    public static final void onCreate$lambda$1$lambda$0(MainActivity this$0, View it) {
        PictorialLiveData<String> liveData;
        TestViewModel testViewModel = this$0.viewModel;
        // 注释1
        liveData.observe(this$0, new MainActivity$sam$androidx_lifecycle_Observer$0(MainActivity$onCreate$1$1$1.INSTANCE));
    }

可以看到注释1的位置,调用liveDataobserve方法,其中的Observer接口是编译器自动生成的一个类MainActivity$sam$androidx_lifecycle_Observer$0,这个类实现了Observer接口,如下面:

java 复制代码
public final class MainActivity$sam$androidx_lifecycle_Observer$0 implements Observer, FunctionAdapter {
    private final /* synthetic */ Function1 function;

    public MainActivity$sam$androidx_lifecycle_Observer$0(Function1 function) {
        Intrinsics.checkNotNullParameter(function, "function");
        this.function = function;
    }
    ......
}

其中的Function1就是我们上面 kotlin 代码中的打印日志的业务逻辑实现,kotlin 会自动根据参数的个数编译成Function1,Function2....FunctionN,也就是上面的MainActivity$onCreate$1$1$1.INSTANCE变量,我们看下它的实现:

java 复制代码
public final class MainActivity$onCreate$1$1$1 extends Lambda implements Function1<String, Unit> {
    // !!!!!!!看到没有,这里是静态变量
    public static final MainActivity$onCreate$1$1$1 INSTANCE = new MainActivity$onCreate$1$1$1();
    ......

    public final void invoke2(String it) {
        Intrinsics.checkNotNullParameter(it, "it");
        Log.i("MainActivity", "return result is " + it);
    }
}

当你看到这个类的时候,估计你就明白为什么上面的日志只打印一次了,因为 kotlin 中每个函数式接口(如果没有引用外部的非静态变量或者对象,这里存在差异),对应 lambda 表达式编译成立一个静态变量,也就是不管你调用了 observe 多少次,他都是同一个对象

所以,在了解了背后的原理之后,我们在使用这种函数式接口的 lambda 方式时要非常注意,涉及到静态变量,稍微不小心,就有可能造成内存泄漏或者逻辑错误。

如果觉得对你有帮助,请点赞关注,或者关注我交流。也可以点击深入学习Kotlin看其他文章,希望一起学习进步,加油!

相关推荐
Estar.Lee21 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯1 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!4 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟5 小时前
Android音频采集
android·音视频
小白也想学C6 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程6 小时前
初级数据结构——树
android·java·数据结构
乐闻x7 小时前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
闲暇部落8 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
青云交9 小时前
大数据新视界 -- 大数据大厂之 Impala 性能优化:跨数据中心环境下的挑战与对策(上)(27 / 30)
大数据·性能优化·impala·案例分析·代码示例·跨数据中心·挑战对策