技术
Android 技术

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

LapisyLapisy
2026年1月10日
50

函数式接口

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

1fun interface IServiceInterface { 2 fun getService() 3}

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

作用

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

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

1 view.setOnClickListener(object : OnClickListener { 2 override fun onClick(v: View?) { 3 TODO("Not yet implemented") 4 } 5 6 }) 7

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

1 view.setOnClickListener { 2 TODO("Not yet implemented") 3 }

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

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

1interface IServiceInterface { 2 fun getService() 3}

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

1 setIServiceInterface(object : IServiceInterface { 2 override fun getService() { 3 print("定义的接口") 4 } 5 6 })

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

1fun interface IServiceInterface { 2 fun getService() 3}

这样的话,就能够实现

1 setIServiceInterface{ 2 print("定义的接口") 3 }

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

1fun interface Observer<T> { 2 3 /** 4 * Called when the data is changed is changed to [value]. 5 */ 6 fun onChanged(value: T) 7}

实际使用中的坑

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

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

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

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

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

1 public static final void onCreate$lambda$1$lambda$0(MainActivity this$0, View it) { 2 PictorialLiveData<String> liveData; 3 TestViewModel testViewModel = this$0.viewModel; 4 // 注释1 5 liveData.observe(this$0, new MainActivity$sam$androidx_lifecycle_Observer$0(MainActivity$onCreate$1$1$1.INSTANCE)); 6 } 7

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

1public final class MainActivity$sam$androidx_lifecycle_Observer$0 implements Observer, FunctionAdapter { 2 private final /* synthetic */ Function1 function; 3 4 public MainActivity$sam$androidx_lifecycle_Observer$0(Function1 function) { 5 Intrinsics.checkNotNullParameter(function, "function"); 6 this.function = function; 7 } 8 ...... 9}

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

1public final class MainActivity$onCreate$1$1$1 extends Lambda implements Function1<String, Unit> { 2 // !!!!!!!看到没有,这里是静态变量 3 public static final MainActivity$onCreate$1$1$1 INSTANCE = new MainActivity$onCreate$1$1$1(); 4 ...... 5 6 public final void invoke2(String it) { 7 Intrinsics.checkNotNullParameter(it, "it"); 8 Log.i("MainActivity", "return result is " + it); 9 } 10}

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

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

评论讨论

评论需要审核后才能显示