希望帮你在Kotlin进阶路上少走弯路,在技术上稳步提升。当然,由于个人知识储备有限,笔记中难免存在疏漏或表述不当的地方,也非常欢迎大家提出宝贵意见,一起交流进步。 —— Android_小雨
整体目录:Kotlin 进阶不迷路:41 个核心知识点,构建完整知识体系
一、前言
1.1 SAM 接口的核心定位
在软件工程的演进中,SAM(Single Abstract Method) 接口扮演着连接 面向对象编程 (OOP) 与 函数式编程 (FP) 的关键桥梁角色。在 OOP 中,我们习惯通过定义接口来规范行为;而在 FP 中,我们倾向于直接传递函数。SAM 接口完美地融合了两者:它既保留了接口的类型定义和语义明确性,又允许开发者以函数式(Lambda)的方式简洁地实现接口,从而简化了单一职责接口的实现成本。
1.2 Kotlin SAM 接口的设计价值
Kotlin 引入(并强化)SAM 接口的设计价值主要体现在三个方面:
- 消除样板代码:用简洁的 Lambda 表达式替代冗长笨重的匿名内部类。
- Java 生态兼容:完美适配 Java 的
@FunctionalInterface,使得 Kotlin 调用 Java 回调库时体验极其丝滑。 - 降低认知负荷:通过
fun interface关键字,让代码意图更加清晰,开发者一眼便知该接口是为了函数式调用而生。
1.3 核心疑问
- 判定标准:到底什么样的接口才算 SAM?普通的单方法接口就是吗?
- 实现原理:为什么写一个 Lambda 就能变成一个接口对象?编译器在背后做了什么?
1.4 本文核心内容
本文将严格遵循“定义规则 → Lambda 简写 → Java 互操作 → 实战场景 → 避坑指南”的逻辑,带你彻底掌握 Kotlin SAM 接口的每一个细节。
二、SAM 接口基础:定义与核心规则
2.1 什么是 SAM 接口?
SAM 是 Single Abstract Method 的缩写。顾名思义,它指的是仅包含一个抽象方法的接口。在 Kotlin 中,这种接口也被称为函数式接口(Functional Interface)。
2.2 定义语法
在 Kotlin 1.4 之前,SAM 转换主要针对 Java 接口。从 Kotlin 1.4 开始,Kotlin 引入了 fun 关键字来显式修饰接口,正式支持 Kotlin 原生的 SAM 转换。
基础语法:
fun interface 接口名 {
fun 抽象方法(参数): 返回值
}
2.3 关键规则
要定义一个合法的 SAM 接口,必须严格遵守以下约束:
- 抽象方法数量严格为 1:这是核心红线。
- 允许包含非抽象成员:默认方法(带方法体)、静态方法(伴生对象内)、属性均可存在,只要抽象的部分只有一处。
- Kotlin 1.4+ 强制要求:对于 Kotlin 定义的接口,必须使用
fun interface修饰才能支持 Lambda 转换。普通 interface 即使只有一个方法,也无法使用 SAM 特性(除非是 Java 定义的接口)。 - 特殊限制:SAM 接口不能是
sealed(密封接口)或其他特殊类型。 - 抽象属性计数:如果接口中包含抽象属性,它也会被视为抽象成员。因此,如果有一个抽象方法和一个抽象属性,它就不是 SAM 接口。
2.4 简单示例
以下是业务开发中几种常见的 SAM 接口定义模式:
// 1. 无参数无返回值(典型场景:点击事件、Runnable)
fun interface OnClickListener {
fun onClick()
}
// 2. 单参数有返回值(典型场景:数据过滤、转换)
fun interface Filter<T> {
fun accept(data: T): Boolean
}
// 3. 多参数有返回值(典型场景:比较器、聚合运算)
fun interface Comparator<T> {
fun compare(a: T, b: T): Int
}
三、核心特性:Lambda 表达式简化 SAM 接口实现
3.1 传统实现方式的痛点
在没有 SAM 转换之前,即使接口只有一个方法,我们也必须使用 object : Interface 的匿名内部类写法:
// 繁琐的匿名内部类写法
val listener = object : OnClickListener {
override fun onClick() {
println("按钮被点击")
}
}
痛点:代码缩进深,包含大量无意义的 object、override 关键字,核心逻辑被淹没在样板代码中。
3.2 SAM 转换原理
SAM 转换(SAM Conversion) 是编译器的魔法。当编译器发现通过 fun interface 定义的接口被实例化,且传入的是一个 Lambda 表达式时,它会自动生成一个该接口的实例,并将 Lambda 的函数体作为那个唯一抽象方法的实现。
3.3 Lambda 简写语法
可以直接将 Lambda 赋值给接口类型,或者在参数传递时直接写 Lambda。
语法格式:
接口名 { 参数 -> 方法体 }
简化示例:
// 此时编译器自动将 Lambda 转换为 OnClickListener 实例
val listener = OnClickListener { println("按钮被点击") }
3.4 不同参数场景的 Lambda 实现技巧
SAM 接口的抽象方法可能包含不同数量的参数和返回值,对应的 Lambda 实现技巧也有所不同。以下针对四种常见场景,讲解 Lambda 的实现技巧:
场景 1:无参数 SAM 接口
抽象方法无参数时,Lambda 体直接编写业务逻辑,无需声明参数。例如 OnClickListener:
// 无参数 SAM 接口
fun interface OnClickListener {
fun onClick()
}
// Lambda 实现:直接写逻辑
val listener = OnClickListener {
println("无参数,直接执行逻辑")
}
场景 2:单参数 SAM 接口
抽象方法有一个参数时,Lambda 中可省略参数名,用 Kotlin 内置的 it 关键字指代该参数,简化代码。例如 Filter<Int>:
// 单参数 SAM 接口
fun interface Filter<Int> {
fun accept(data: Int): Boolean
}
// Lambda 实现:用 it 指代参数 data
val evenFilter = Filter { it % 2 == 0 } // 等价于 { data -> data % 2 == 0 }
println(evenFilter.accept(4)) // 输出:true
println(evenFilter.accept(5)) // 输出:false
若参数名有明确语义,也可显式声明参数名,增强可读性。
场景 3:多参数 SAM 接口
抽象方法有多个参数时,需在 Lambda 中显式声明参数名,参数之间用逗号分隔,箭头 -> 后编写逻辑。例如 Comparator<String>:
// 多参数 SAM 接口
fun interface Comparator<String> {
fun compare(a: String, b: String): Int
}
// Lambda 实现:显式声明参数 a、b
val lengthComparator = Comparator { a, b ->
// 比较字符串长度,返回差值
a.length - b.length
}
println(lengthComparator.compare("apple", "banana")) // 输出:-1(apple 长度 5 < banana 长度 6)
场景 4:带返回值 SAM 接口
抽象方法有返回值时,Lambda 体的最后一行表达式会自动作为返回值,无需显式编写 return 关键字(若用 return 需加标签,否则会退出外层函数)。例如 Filter<String>:
// 带返回值的 SAM 接口
fun interface Filter<String> {
fun accept(data: String): Boolean
}
// Lambda 实现:最后一行表达式作为返回值
val longStrFilter = Filter {
val minLength = 5
// 最后一行:data.length > minLength 作为返回值
it.length > minLength
}
println(longStrFilter.accept("apple")) // 输出:true(长度 5 不大于 5,此处应为 false?修正:it.length >= minLength 则为 true)
// 修正后示例
val longStrFilterCorrect = Filter { it.length >= 5 }
println(longStrFilterCorrect.accept("apple")) // 输出:true
println(longStrFilterCorrect.accept("pear")) // 输出:false
若返回值类型为 Unit(无返回值),Lambda 体可直接编写逻辑,无需考虑返回值。
3.5 匹配关键细节
在编写 Lambda 时,必须注意:
- 参数一致性:Lambda 的参数数量和类型必须与接口抽象方法完全匹配。
- 类型推导:如果上下文(如变量类型、函数参数类型)已明确是某个 SAM 接口,Lambda 可以省略接口名构造器(主要用于 Java 互操作或高阶函数传参)。
- 注:对于 Kotlin 定义的
fun interface,赋值给变量时通常建议显式带上接口名构造器InterfaceName { }以利用 SAM 构造函数。
- 注:对于 Kotlin 定义的
四、Kotlin 与 Java 的 SAM 互操作
这是 Kotlin 极具优势的特性之一。
4.1 Java 函数式接口在 Kotlin 中的使用
Java 中任何单一抽象方法的接口(无论是否标注 @FunctionalInterface),在 Kotlin 中都会被自动视为 SAM 接口。
Java 定义:
public interface JavaCallback {
void onSuccess(String data);
}
Kotlin 使用: Kotlin 编译器支持自动类型适配,你甚至不需要像调用 Kotlin SAM 那样使用构造函数,直接传递 Lambda 即可。
// 显式类型声明,右侧直接写 Lambda
val callback: JavaCallback = { data -> println("接收数据:$data") }
// 或者在方法参数中
fun useJava(cb: JavaCallback) { ... }
useJava { println(it) } // 极其简洁
4.2 Kotlin SAM 接口在 Java 中的使用
这是 fun interface 的反向兼容优势。
- 如果使用了
fun interface:Kotlin 编译器会生成一个静态的.create()方法(或者兼容 Java 的 Lambda 转换机制),使得 Java 代码也可以方便地使用它。 - 如果没有使用
fun:普通的 Kotlin 接口在 Java 中只能通过new Interface() { ... }匿名内部类来实现,无法使用 Java 的 Lambda 语法。
Kotlin 定义:
fun interface KotlinProcessor {
fun process(data: String): String
}
Java 使用:
// Java 能够识别这是个函数式接口,支持 Lambda
KotlinProcessor processor = data -> data.toUpperCase();
// 或者使用生成的辅助方法(视版本和混淆而定,通常直接 Lambda 即可)
4.3 互操作注意事项
- 单向更灵活:Kotlin 调用 Java SAM 几乎是无感知的;Java 调用 Kotlin SAM 强烈依赖
fun interface关键字。 - 泛型擦除:在 Java 和 Kotlin 混用泛型 SAM 接口时,有时需要显式声明类型,避免类型推导在边界情况下失败。
五、SAM 接口的进阶用法
5.1 带默认方法的 SAM 接口
SAM 限制的是抽象方法只有一个,不限制默认方法。
// 带默认方法的 SAM 接口
fun interface Logger {
// 核心抽象方法:定制日志输出方式(SAM 核心)
fun log(message: String)
// 默认方法 1:封装错误日志的格式
fun logError(message: String) {
// 调用抽象方法输出格式化后的错误日志
log("[ERROR] ${System.currentTimeMillis()} - $message")
}
// 默认方法 2:封装信息日志的格式
fun logInfo(message: String) {
log("[INFO] ${System.currentTimeMillis()} - $message")
}
// 默认方法 3:封装调试日志的格式(支持 Throwable 信息)
fun logDebug(message: String, throwable: Throwable? = null) {
val debugMsg = if (throwable != null) {
"[DEBUG] ${System.currentTimeMillis()} - $message - ${throwable.message}"
} else {
"[DEBUG] ${System.currentTimeMillis()} - $message"
}
log(debugMsg)
}
}
// 使用示例:Lambda 仅实现核心 log 方法,直接调用默认方法
fun main() {
// 1. 实现控制台日志输出(Lambda 仅写核心输出逻辑)
val consoleLogger = Logger { message ->
println(message)
}
// 2. 直接调用默认方法,无需自己实现格式逻辑
consoleLogger.logInfo("应用启动成功")
consoleLogger.logError("数据库连接失败")
consoleLogger.logDebug("用户登录", NullPointerException("会话为空"))
// 3. 实现文件日志输出(仅修改核心输出逻辑,复用默认格式)
val fileLogger = Logger { message ->
// 模拟写入文件的逻辑
java.io.File("app.log").appendText("$message\n")
}
fileLogger.logInfo("用户张三登录成功")
}
运行上述代码,控制台会输出:
[INFO] 1717248000000 - 应用启动成功
[ERROR] 1717248000001 - 数据库连接失败
[DEBUG] 1717248000002 - 用户登录 - 会话为空
同时 app.log 文件会新增内容:[INFO] 1717248000003 - 用户张三登录成功。可以看出,默认方法复用了日志格式逻辑,Lambda 仅需聚焦核心的输出逻辑,大幅提升了代码复用性。
5.2 带静态方法的 SAM 接口
SAM 接口可以通过伴生对象(companion object)定义静态方法,静态方法通常用于封装与 SAM 接口相关的工具逻辑,如创建默认实例、提供常用实现等。静态方法的存在不会影响 SAM 接口的判定,也不会干扰 Lambda 简化实现。
例如数据映射场景,可在 Mapper SAM 接口的伴生对象中定义静态方法,提供“恒等映射”“字符串转整数”等常用实现,方便开发者直接使用。
// 带静态方法的 SAM 接口(通过伴生对象实现)
fun interface Mapper<in T, out R> {
// 核心抽象方法:将 T 类型映射为 R 类型
fun map(data: T): R
// 伴生对象:定义静态方法
companion object {
// 静态方法 1:创建恒等映射(输入类型=输出类型)
fun <T> identity(): Mapper<T, T> {
return Mapper { it } // Lambda 实现恒等映射
}
// 静态方法 2:创建字符串转整数的映射(处理空值)
fun stringToInt(default: Int = 0): Mapper<String, Int> {
return Mapper { str ->
str.toIntOrNull() ?: default
}
}
// 静态方法 3:创建整数转字符串的映射(指定格式)
fun intToString(pattern: String = "%d"): Mapper<Int, String> {
return Mapper { int ->
String.format(pattern, int)
}
}
}
}
// 使用示例:调用静态方法获取常用映射实例
fun main() {
// 1. 恒等映射(输入=输出)
val identityMapper = Mapper.identity<String>()
println(identityMapper.map("test")) // 输出:test
// 2. 字符串转整数(空值返回默认 0)
val strToIntMapper = Mapper.stringToInt()
println(strToIntMapper.map("123")) // 输出:123
println(strToIntMapper.map("abc")) // 输出:0(转换失败返回默认值)
// 3. 整数转字符串(指定格式为两位小数)
val intToStrMapper = Mapper.intToString("%.2f")
println(intToStrMapper.map(10)) // 输出:10.00
println(intToStrMapper.map(15)) // 输出:15.00
// 4. 自定义映射(Lambda 实现)
val lengthMapper = Mapper<String, Int> { it.length }
println(lengthMapper.map("kotlin")) // 输出:6
}
静态方法的存在让 SAM 接口不仅是“行为抽象”,还具备了“工具类”的特性,开发者无需重复编写常用实现,直接调用静态方法即可获取实例,提升开发效率。
5.3 SAM 接口与函数类型的相互转换
5.3.1 SAM 接口转换为函数类型
SAM 接口实例可通过“SAM 实例::抽象方法名”的方法引用方式,转换为对应的函数类型。例如 Adder SAM 接口的抽象方法为 add(a: Int, b: Int): Int,其实例可转换为 (Int, Int) -> Int 函数类型。
// SAM 接口转函数类型示例
fun interface Adder {
fun add(a: Int, b: Int): Int
}
// 高阶函数:接收函数类型参数
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
fun main() {
// 1. 创建 SAM 接口实例
val adder: Adder = { a, b -> a + b }
// 2. 将 SAM 接口实例转换为函数类型(方法引用)
val addFunction: (Int, Int) -> Int = adder::add
// 3. 传递函数类型参数给高阶函数
val sum = calculate(10, 20, addFunction)
println(sum) // 输出:30
// 4. 直接传递 SAM 接口实例(编译器自动转换为函数类型?不,需显式转换)
// 错误示例:calculate(10, 20, adder) // 编译报错,类型不匹配
// 正确方式:显式转换后传递
val result = calculate(10, 20, adder::add)
println(result) // 输出:30
}
5.3.2 函数类型转换为 SAM 接口
函数类型变量可通过“函数变量::invoke”的方法引用方式,转换为对应的 SAM 接口实例。例如 (Int, Int) -> Int 函数类型,可转换为 Adder SAM 接口实例。
// 函数类型转 SAM 接口示例
fun interface Adder {
fun add(a: Int, b: Int): Int
}
// 函数类型变量
val multiplyFunction: (Int, Int) -> Int = { a, b -> a * b }
fun main() {
// 1. 将函数类型转换为 SAM 接口实例(方法引用 invoke 方法)
val multiplyAdder: Adder = multiplyFunction::invoke
// 2. 调用 SAM 接口方法
val product = multiplyAdder.add(10, 20)
println(product) // 输出:200
// 3. 直接创建函数类型并转换
val subtractFunction: (Int, Int) -> Int = { a, b -> a - b }
val subtractAdder: Adder = subtractFunction::invoke
println(subtractAdder.add(50, 30)) // 输出:20
}
通过这种相互转换,SAM 接口与函数类型可以无缝配合,既可以利用 SAM 接口的接口特性(如默认方法、静态方法、兼容 Java),又可以享受函数类型在 Kotlin 原生高阶函数中的灵活使用。
5.4 高阶函数中使用 SAM 接口
SAM 接口常作为高阶函数的参数,用于封装回调逻辑或业务逻辑,调用高阶函数时可通过 Lambda 简化 SAM 接口的实现,让代码更简洁。这种方式在数据处理、异步任务等场景中非常常见。
例如数据过滤场景,定义一个接收列表和 Filter SAM 接口的高阶函数 processData,调用时直接传递 Lambda 实现过滤逻辑,无需创建匿名内部类。
// 高阶函数中使用 SAM 接口作为参数
fun interface Filter<T> {
fun accept(data: T): Boolean
}
// 高阶函数:接收数据列表和 Filter 接口,返回过滤后的列表
fun <T> processData(data: List<T>, filter: Filter<T>): List<T> {
val result = mutableListOf<T>()
for (item in data) {
if (filter.accept(item)) {
result.add(item)
}
}
return result
}
// 扩展:结合默认方法和高阶函数,实现更复杂的处理
fun interface Processor<T, R> {
fun process(item: T): R
// 默认方法:批量处理列表
fun processList(list: List<T>): List<R> {
return list.map { process(it) }
}
}
// 使用示例
fun main() {
// 1. 过滤整数列表(保留偶数)
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenNumbers = processData(numbers) { it % 2 == 0 }
println("偶数列表:$evenNumbers") // 输出:偶数列表:[2, 4, 6, 8, 10]
// 2. 过滤字符串列表(保留长度大于 5 的字符串)
val strings = listOf("apple", "banana", "cherry", "date", "elderberry")
val longStrings = processData(strings) { it.length > 5 }
println("长字符串列表:$longStrings") // 输出:长字符串列表:[banana, cherry, elderberry]
// 3. 用 Processor 批量处理(字符串转大写)
val upperCaseProcessor = Processor<String, String> { it.toUpperCase() }
val upperCaseStrings = upperCaseProcessor.processList(strings)
println("大写字符串列表:$upperCaseStrings") // 输出:大写字符串列表:[APPLE, BANANA, CHERRY, DATE, ELDERBERRY]
// 4. 组合过滤和处理(先过滤再转换)
val filteredAndProcessed = processData(strings) { it.length > 5 }
.let { upperCaseProcessor.processList(it) }
println("过滤并转换后的列表:$filteredAndProcessed") // 输出:过滤并转换后的列表:[BANANA, CHERRY, ELDERBERRY]
}
可以看出,SAM 接口作为高阶函数参数时,Lambda 简化让调用代码非常简洁,核心的业务逻辑(如过滤条件、转换规则)一目了然,大幅提升了代码的可读性和开发效率。
六、实用场景与实战示例
6.1 事件回调场景
这是 Android 或 UI 开发中最常见的场景。
// 定义:下载回调
fun interface DownloadListener {
fun onProgress(percent: Int)
}
// 模拟下载器
class Downloader {
fun download(url: String, listener: DownloadListener) {
// 模拟进度
listener.onProgress(50)
listener.onProgress(100)
}
}
// 实战调用
val downloader = Downloader()
// 以前需要 new DownloadListener... 现在只需要:
downloader.download("https://example.com") { percent ->
println("当前下载进度:$percent%")
}
6.2 数据处理场景 (Strategy 模式)
策略模式的轻量化实现。
fun interface DiscountStrategy {
fun calculate(price: Double): Double
}
class ShoppingCart(private val strategy: DiscountStrategy) {
fun checkout(price: Double) = strategy.calculate(price)
}
// 使用
val vipCart = ShoppingCart { price -> price * 0.8 } // VIP 8折
val normalCart = ShoppingCart { it } // 原价
6.3 命令模式场景
fun interface Command {
fun execute()
}
val history = mutableListOf<Command>()
fun runAndRecord(cmd: Command) {
cmd.execute()
history.add(cmd)
}
// 极简调用
runAndRecord { println("保存文件") }
runAndRecord { println("发送邮件") }
七、与相关概念的核心区别
7.1 SAM 接口 vs 普通接口
| 维度 | SAM 接口 (fun interface) | 普通接口 (interface) |
|---|---|---|
| 定义关键字 | fun interface | interface |
| 抽象方法数 | 严格为 1 个 | 0 到 N 个 |
| Lambda 实例化 | 支持 (编译器自动转换) | 不支持 (必须用 object :) |
| 语义 | 强调“行为”或“动作” | 强调“契约”或“能力集合” |
7.2 SAM 接口 vs 函数类型 ((T) -> R)
这是一个常见的设计抉择:参数类型是写 fun interface Action 还是 () -> Unit?
| 维度 | SAM 接口 | 函数类型 |
|---|---|---|
| 可读性 | 高。有明确的名字(如 OnClickListener),语义清晰。 | 中。只是参数签名的描述,无法体现业务含义。 |
| Java 互操作 | 好。Java 能识别为接口。 | 差。Java 中表现为 FunctionN 类,调用麻烦。 |
| 扩展性 | 强。可添加默认方法、伴生对象。 | 无。只能是纯函数。 |
| 性能 | 实例化通常有微小开销(但在 Inline 上下文中可优化)。 | 与 SAM 类似,但如果是内联函数 (inline) 则均为零开销。 |
| 序列化 | 较容易实现 Serializable。 | 难以序列化。 |
结论:如果只是简单的内部回调,函数类型更方便;如果需要对外暴露 API、兼容 Java 或定义具有明确业务含义的行为,SAM 接口更好。
八、使用注意事项与避坑点
8.1 避免违反 SAM 核心规则
如果你在一个 fun interface 中添加了第二个抽象方法,编译器会直接报错。如果你去掉了 fun 关键字,现有的 Lambda 调用代码会全部变红,提示无法转换。
8.2 低版本兼容问题
- Kotlin 1.4 引入了
fun interface。 - 在旧版本 Kotlin 中,只能利用 Java 定义的接口来实现 SAM 转换。
- 如果你维护的是旧项目,需注意版本差异。
8.3 Lambda 与抽象方法匹配问题
Lambda 的返回值是隐式的。如果接口方法要求返回 Boolean,而你的 Lambda 最后一行是 println()(返回 Unit),编译器会报错。
错误示例:
fun interface Predicate { fun test(i: Int): Boolean }
// 错误:Lambda 推导返回 Unit,但接口需要 Boolean
val p = Predicate { println(it) }
8.4 避免过度设计
不要为了使用 Lambda 而强行把本该包含多个方法的接口拆分成多个 SAM 接口。这会导致接口碎片化,不仅没有简化代码,反而增加了系统的复杂性。
错误示范:
// 不推荐:为了凑 SAM 强行拆分
fun interface OnSuccess { fun success() }
fun interface OnFailure { fun fail() }
fun request(s: OnSuccess, f: OnFailure) { ... } // 调用时参数列表太长
正确示范:
// 推荐:相关联的方法放在同一个常规接口中
interface RequestListener {
fun success()
fun fail()
}
8.5 闭包捕获外部变量风险
与所有 Lambda 表达式一样,SAM 转换后的 Lambda 如果捕获了外部的可变变量(var),在多线程环境下需要注意线程安全问题。
var counter = 0
// 如果这个 Runnable 在多线程中执行,counter 的操作是不安全的
val r = Runnable { counter++ }
8.6 序列化注意事项
如果你需要将 SAM 接口实例序列化(例如在 Android 中通过 Intent 传递 Serializable 对象),通过 Lambda 简写创建的实例通常很难保证序列化的稳定性(因为 Lambda 的类名是编译器动态生成的)。
建议:若接口实例需要序列化,建议使用具体的类实现或 object 显式实现 Serializable,而不是直接传递 Lambda。
九、总结与最佳实践
9.1 核心知识点回顾
- 关键字:
fun interface。 - 条件:仅有一个抽象方法。
- 核心功能:允许使用 Lambda 表达式直接实例化接口。
9.2 最佳实践
- 优先使用
fun interface:只要接口符合 SAM 标准(单一抽象方法),就加上fun,给调用者提供 Lambda 简写的便利。 - API 设计原则:
- 公用库/SDK:优先用 SAM 接口(命名清晰,Java 兼容性好)。
- 私有辅助函数:优先用函数类型
() -> Unit(写法最简,无须定义新接口)。
- 命名规范:SAM 接口的名字通常是动词或形容词结尾,如
Listener,Callback,Predicate,Strategy,以体现其行为特性。
9.3 选型建议
- 场景 A:仅 Kotlin 内部使用、逻辑简单的一次性回调 → 函数类型
(T) -> Unit。 - 场景 B:需兼容 Java、或者该回调具有很强的业务领域概念(如
PaymentValidator) → SAM 接口。 - 场景 C:包含多个相关联的回调方法(如
onSuccess+onFailure) → 普通接口interface。
10. 全文总结
为了方便记忆,我们将 Kotlin 接口的核心精华总结为以下 “1-2-3-4”法则:
- 1 个核心约束:SAM 接口必须且只能包含 1 个抽象方法(但可包含任意默认/静态方法)。
- 2 种实现形式:既支持传统的 匿名内部类 写法,也支持简洁的 Lambda 表达式写法。
- 3 大设计价值:消除样板代码(简洁)、语义清晰(具名)、Java 完美互操作(兼容)。
- 4 个避坑指南:
- 记得加
fun关键字(Kotlin 1.4+)。 - 参数类型必须严格匹配。
- 不要在 Lambda 中显式
return。 - 涉及序列化或多方法契约时慎用。
- 记得加