22.Kotlin 接口:接口进阶:SAM (单一抽象方法) 接口

60 阅读12分钟

希望帮你在Kotlin进阶路上少走弯路,在技术上稳步提升。当然,由于个人知识储备有限,笔记中难免存在疏漏或表述不当的地方,也非常欢迎大家提出宝贵意见,一起交流进步。 —— Android_小雨

整体目录:Kotlin 进阶不迷路:41 个核心知识点,构建完整知识体系

一、前言

1.1 SAM 接口的核心定位

在软件工程的演进中,SAM(Single Abstract Method) 接口扮演着连接 面向对象编程 (OOP)函数式编程 (FP) 的关键桥梁角色。在 OOP 中,我们习惯通过定义接口来规范行为;而在 FP 中,我们倾向于直接传递函数。SAM 接口完美地融合了两者:它既保留了接口的类型定义和语义明确性,又允许开发者以函数式(Lambda)的方式简洁地实现接口,从而简化了单一职责接口的实现成本。

1.2 Kotlin SAM 接口的设计价值

Kotlin 引入(并强化)SAM 接口的设计价值主要体现在三个方面:

  1. 消除样板代码:用简洁的 Lambda 表达式替代冗长笨重的匿名内部类。
  2. Java 生态兼容:完美适配 Java 的 @FunctionalInterface,使得 Kotlin 调用 Java 回调库时体验极其丝滑。
  3. 降低认知负荷:通过 fun interface 关键字,让代码意图更加清晰,开发者一眼便知该接口是为了函数式调用而生。

1.3 核心疑问

  • 判定标准:到底什么样的接口才算 SAM?普通的单方法接口就是吗?
  • 实现原理:为什么写一个 Lambda 就能变成一个接口对象?编译器在背后做了什么?

1.4 本文核心内容

本文将严格遵循“定义规则 → Lambda 简写 → Java 互操作 → 实战场景 → 避坑指南”的逻辑,带你彻底掌握 Kotlin SAM 接口的每一个细节。

二、SAM 接口基础:定义与核心规则

2.1 什么是 SAM 接口?

SAMSingle 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. 抽象方法数量严格为 1:这是核心红线。
  2. 允许包含非抽象成员:默认方法(带方法体)、静态方法(伴生对象内)、属性均可存在,只要抽象的部分只有一处。
  3. Kotlin 1.4+ 强制要求:对于 Kotlin 定义的接口,必须使用 fun interface 修饰才能支持 Lambda 转换。普通 interface 即使只有一个方法,也无法使用 SAM 特性(除非是 Java 定义的接口)。
  4. 特殊限制:SAM 接口不能是 sealed(密封接口)或其他特殊类型。
  5. 抽象属性计数:如果接口中包含抽象属性,它也会被视为抽象成员。因此,如果有一个抽象方法和一个抽象属性,它就不是 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("按钮被点击")
    }
}

痛点:代码缩进深,包含大量无意义的 objectoverride 关键字,核心逻辑被淹没在样板代码中。

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 与 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 互操作注意事项

  1. 单向更灵活:Kotlin 调用 Java SAM 几乎是无感知的;Java 调用 Kotlin SAM 强烈依赖 fun interface 关键字。
  2. 泛型擦除:在 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 interfaceinterface
抽象方法数严格为 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 最佳实践

  1. 优先使用 fun interface:只要接口符合 SAM 标准(单一抽象方法),就加上 fun,给调用者提供 Lambda 简写的便利。
  2. API 设计原则
    • 公用库/SDK:优先用 SAM 接口(命名清晰,Java 兼容性好)。
    • 私有辅助函数:优先用函数类型 () -> Unit(写法最简,无须定义新接口)。
  3. 命名规范: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 个避坑指南
    1. 记得加 fun 关键字(Kotlin 1.4+)。
    2. 参数类型必须严格匹配。
    3. 不要在 Lambda 中显式 return
    4. 涉及序列化或多方法契约时慎用。