Kotlin 库作者 API 设计指南¶
写应用代码和写库 API 是两种不同工作。应用代码只服务当前系统,改错后一起发布即可;库 API 一旦被别人依赖,就要考虑源码兼容、二进制兼容、行为兼容、Java 调用方式、文档和废弃策略。Kotlin 官方 API guidelines 专门为库作者整理了可读性、一致性、可预测性、向后兼容等建议,本章把这些建议转成中文实践清单,并补充 Java 对比。
三种兼容性¶
官方指南把向后兼容分成三类:
- Binary compatibility:调用方不重新编译,只替换你的新版本库,旧的已编译代码仍能运行。
- Source compatibility:调用方源码不用改,但可能需要重新编译。
- Behavioral compatibility:同样输入、同样场景下,语义没有意外变化,除非是修复 bug。
Java 对比:Java 库作者也要关注这三类兼容。Kotlin 的额外复杂度在于默认参数、数据类、顶层函数、属性、协程、inline 函数、值类等特性会生成不总是直观的 JVM 字节码。
一个变化是否安全,不能只看 Kotlin 源码“能不能编译”,还要看老版本调用方是否能在不重新编译时继续运行。
使用 Binary Compatibility Validator¶
JetBrains 提供 Binary Compatibility Validator Gradle 插件。它的核心任务包括:
apiDump:生成描述公开 API 的.api文件。apiCheck:把当前构建产物与已保存的.api文件比较。
建议库项目把 apiCheck 纳入 CI:
./gradlew apiCheck
当你有意修改公开 API:
./gradlew apiDump
然后把 .api 差异当成代码评审的一部分,而不是自动接受。
这类似 Java 项目使用 japicmp、revapi 等工具,只是 Kotlin 官方指南明确推荐了适配 Kotlin API 的验证工具。
公共 API 显式写返回类型¶
应用内部代码可以依赖类型推断:
private fun normalize(name: String) = name.trim().lowercase()
但公共 API 应该显式写返回类型:
public fun normalizeName(name: String): String =
name.trim().lowercase()
原因:
- 类型推断结果可能因为实现变化而改变。
- 调用方依赖的是 API 契约,不是你的当前实现。
- 公开函数、属性、扩展函数、接口方法都应该让签名稳定。
危险示例:
public fun ids() = listOf(1, 2, 3)
如果以后实现改成:
public fun ids() = setOf(1, 2, 3)
源码看起来只是换了集合构造,但公开返回类型可能从 List<Int> 变成 Set<Int>,这就是 API 变化。更稳妥:
public fun ids(): List<Int> = listOf(1, 2, 3)
Java 对比:Java 方法必须写返回类型,所以这类风险较少。Kotlin 的类型推断很方便,但库 API 中便利性不应该压过稳定性。
谨慎修改默认参数¶
Kotlin 默认参数对应用代码非常舒服:
fun connect(
host: String,
port: Int = 5432,
ssl: Boolean = true,
)
但对公开库 API,默认参数会生成额外调用结构,并影响 Java 调用方式。随意新增、删除、重排参数可能破坏二进制兼容或源码兼容。
不建议:
// v1
fun createClient(host: String, timeoutMillis: Long = 1_000): Client
// v2: 在中间加参数
fun createClient(host: String, retries: Int = 3, timeoutMillis: Long = 1_000): Client
更稳妥:
fun createClient(host: String): Client =
createClient(ClientConfig(host = host))
fun createClient(config: ClientConfig): Client
配置对象比长参数列表更可演进。新增配置项时可以给 ClientConfig 添加属性,但如果 ClientConfig 是公开 data class,仍然要考虑二进制兼容,见下一节。
Java 对比:Java 常用重载、Builder 或配置对象表达可选参数。Kotlin 默认参数更简洁,但公开 API 不能只考虑 Kotlin 调用方。
不要随意暴露 data class¶
数据类适合应用 DTO 和内部值对象,但公开库 API 中要谨慎使用。
data class User(
val id: Long,
val name: String,
)
数据类会生成:
componentN()。copy()。equals()、hashCode()、toString()。- 主构造函数参数对应的属性。
如果你在新版本中新增一个主构造函数属性:
data class User(
val id: Long,
val name: String,
val email: String,
)
调用方可能受到影响:
- 解构声明数量变化。
copy()签名变化。- 构造调用变化。
- 二进制兼容风险。
公开库里更稳妥的做法:
class User private constructor(
val id: Long,
val name: String,
) {
companion object {
fun of(id: Long, name: String): User = User(id, name)
}
}
或者用接口隐藏实现:
interface User {
val id: Long
val name: String
}
private data class DefaultUser(
override val id: Long,
override val name: String,
) : User
Java 对比:Java record 也会把组件作为公开契约,新增 record component 同样不是小改动。Kotlin data class 与 Java record 都适合值建模,但公开库中要谨慎演进。
Boolean 参数要表达含义¶
差的 API:
fun render(markdown: String, trueForHtml: Boolean)
调用方看到:
render(text, true)
无法知道 true 是什么意思。
更好的写法:
enum class OutputFormat {
Html,
PlainText,
}
fun render(markdown: String, format: OutputFormat): String
或者拆成两个函数:
fun renderHtml(markdown: String): String
fun renderPlainText(markdown: String): String
Kotlin 有命名参数:
render(markdown = text, escapeHtml = true)
但 Java 调用方没有 Kotlin 命名参数,且 Kotlin 调用方也可能在位置参数中传布尔值。公开 API 不要依赖调用者永远写命名参数。
用类型表达领域概念¶
差的 API:
fun findOrder(id: String): Order
fun findUser(id: String): User
调用方很容易传错 id。
更好的写法:
@JvmInline
value class OrderId(val value: String)
@JvmInline
value class UserId(val value: String)
fun findOrder(id: OrderId): Order
fun findUser(id: UserId): User
值类可以减少包装对象成本,并让类型系统阻止误传。限制是:
- 对 Java 调用方的体验要验证。
- 泛型、反射、序列化和框架集成时要确认行为。
- 公开 API 中引入值类也属于契约,需要兼容性评估。
Java 对比:Java 可以用小类或 record 包装 id,但成本和语法更重。Kotlin value class 让这种建模更轻。
扩展函数适合增强,不适合隐藏核心能力¶
扩展函数适合给已有类型增加便利方法:
fun String.toUserId(): UserId = UserId(this)
但不要把核心业务能力藏在难以发现的扩展里:
fun HttpClient.syncAllUsers() {
// 大量业务逻辑
}
更好的方式是公开明确服务:
class UserSyncService(
private val httpClient: HttpClient,
) {
fun syncAllUsers() {
// ...
}
}
扩展函数的 Java 调用体验也较差。Java 侧会看到静态方法,而不是成员方法。如果你的库有大量 Java 使用者,核心 API 应该优先设计成 Java 也清楚的类和方法。
Inline 与 PublishedApi¶
inline 函数会把函数体复制到调用点,因此公开 inline API 的实现细节会变成调用方编译产物的一部分。
常见风险:
- 修改 inline 函数内部调用的
internalAPI 可能影响二进制兼容。 - 调用方不重新编译时,旧逻辑可能仍留在调用方字节码中。
- inline 适合高阶函数性能优化和 reified 泛型,但不应该为了“可能更快”随意使用。
当公开 inline 函数需要调用内部成员时,可能需要 @PublishedApi 暴露给内联代码:
@PublishedApi
internal fun parseInternal(input: String): Parsed =
Parsed(input.trim())
public inline fun <reified T> parse(input: String): T {
val parsed = parseInternal(input)
return convert<T>(parsed)
}
@PublishedApi internal 虽然语法上仍是 internal,但对二进制兼容来说已经接近公开契约。修改前要当成 API 评审。
废弃 API 要给迁移路径¶
不要直接删除 API。先废弃,再给替代方案,再经过版本周期删除。
@Deprecated(
message = "Use createClient(ClientConfig) instead.",
replaceWith = ReplaceWith("createClient(ClientConfig(host = host))"),
level = DeprecationLevel.WARNING,
)
fun createClient(host: String): Client =
createClient(ClientConfig(host = host))
升级策略可以分阶段:
WARNING:提示调用方迁移。ERROR:阻止新代码继续使用,但二进制仍保留。- 删除:只在主版本升级或明确破坏性版本中执行。
Java 对比:Java 的 @Deprecated 可以提示废弃,但 Kotlin 的 ReplaceWith 能让 IDE 提供自动替换,迁移体验更好。
实验性 API 使用 RequiresOptIn¶
当 API 还不稳定,但你又想让用户试用,可以使用 opt-in:
@RequiresOptIn(
message = "This API is experimental and may change in future releases.",
level = RequiresOptIn.Level.WARNING,
)
annotation class ExperimentalSearchApi
@ExperimentalSearchApi
fun semanticSearch(query: String): List<SearchResult> = emptyList()
调用方需要显式选择:
@OptIn(ExperimentalSearchApi::class)
fun runSearch() {
semanticSearch("kotlin")
}
这比在 README 里写“实验性”更可靠,因为编译器会参与提醒。
API 可组合性¶
官方可读性指南强调 API 应该易于组合。差的 API 会一次性做太多事:
fun loadValidateTransformAndSend(path: String)
更可组合:
fun load(path: String): RawDocument
fun validate(document: RawDocument): ValidationResult
fun transform(document: RawDocument): Message
fun send(message: Message): SendResult
调用方可以自行组合,也更容易测试。对于应用代码,一站式函数可能方便;对于库 API,分解后的能力更可复用。
DSL 要克制¶
Kotlin 很适合写 DSL:
client {
timeoutMillis = 1_000
retry {
maxAttempts = 3
}
}
DSL 的优势是可读性强,适合配置和结构化声明。风险是:
- IDE 跳转和搜索不如普通函数直观。
- 嵌套层级太深会隐藏作用域。
- Java 调用方几乎无法享受同等体验。
- DSL 设计一旦发布,修改接收者类型和嵌套结构也会影响兼容性。
建议:
- 配置型 API 可以用 DSL。
- 核心业务操作优先普通函数。
- DSL builder 最终生成明确的不可变配置对象。
- 给 Java 用户提供 builder 或构造函数替代入口。
Java 互操作清单¶
面向 JVM 发布库时,检查这些点:
- 顶层函数是否需要
@file:JvmName给 Java 侧更好的类名。 - 对象单例中的静态入口是否需要
@JvmStatic。 - 默认参数是否需要
@JvmOverloads。 - 属性暴露给 Java 后 getter/setter 名称是否合理。
internal在 JVM 字节码中不是 Java 意义上的私有边界,不能当安全隔离。suspend fun是否需要 Java-friendly wrapper。- Kotlin
List只读接口传给 Java 后仍可能被 Java 视角修改底层对象,要做防御性复制。 - 可空性注解是否能被 Java 和 Kotlin 调用方正确理解。
示例:
@file:JvmName("Clients")
package com.example.client
object ClientFactory {
@JvmStatic
@JvmOverloads
fun create(
endpoint: String,
timeoutMillis: Long = 1_000,
): Client = Client(endpoint, timeoutMillis)
}
Java 调用:
Client client = ClientFactory.create("https://api.example.com");
如果使用顶层函数:
@file:JvmName("Clients")
package com.example.client
fun createClient(endpoint: String): Client =
Client(endpoint, timeoutMillis = 1_000)
Java 调用:
Client client = Clients.createClient("https://api.example.com");
文档和示例也是 API 的一部分¶
公开库至少应该提供:
- README 快速开始。
- Dokka API 文档。
- Kotlin 示例。
- 如果支持 Java,提供 Java 示例。
- 版本兼容说明。
- 废弃 API 的迁移说明。
- 错误处理和线程安全说明。
文档中不要只写“调用此函数”。对库用户有价值的是边界条件:
- 什么时候返回空。
- 是否抛异常。
- 是否阻塞。
- 是否线程安全。
- 是否缓存。
- 是否重试。
- 是否保持顺序。
- 是否稳定排序。
发布前检查清单¶
发布 Kotlin 库前,建议逐项检查:
- 公共 API 都有显式返回类型。
- CI 运行
apiCheck。 - 公开数据类经过兼容性评审。
- 默认参数没有破坏旧调用方。
- 废弃 API 有
ReplaceWith或迁移说明。 - 实验性 API 有
@RequiresOptIn。 - Java 调用入口经过实际编译验证。
- Dokka 文档可以生成。
- README 示例可以复制运行。
- 多平台库检查各 target 的 API 差异。
- 版本号符合语义化发布策略。
常见误区¶
误区一:源码兼容就够了¶
很多用户不会在升级依赖时立刻重新编译所有模块。二进制兼容破坏会在运行期表现为 NoSuchMethodError、NoSuchFieldError 等问题。
误区二:Kotlin-only 库不用考虑 Java¶
只要发布到 JVM 生态,就可能被 Java、Groovy、Scala、构建工具、反射框架调用。除非明确声明不支持 Java,否则至少要检查 Java 侧体验。
误区三:internal 就不是 API¶
Kotlin 的 internal 是模块级可见性,但在 JVM 字节码层仍会有可见成员和名称改写。公开 inline 函数调用的 internal 成员尤其要谨慎。
误区四:API 越 Kotlin 风格越好¶
Kotlin 风格不是越多 DSL、扩展、操作符越好。好的库 API 应该清楚、稳定、可组合、可测试,并让目标用户容易调用。如果用户大量来自 Java,Java 体验也是设计目标。
官方参考¶
- Kotlin API guidelines introduction:https://kotlinlang.org/docs/api-guidelines-introduction.html
- Backward compatibility guidelines:https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html
- API readability guidelines:https://kotlinlang.org/docs/api-guidelines-readability.html
- API consistency guidelines:https://kotlinlang.org/docs/api-guidelines-consistency.html
- API predictability guidelines:https://kotlinlang.org/docs/api-guidelines-predictability.html