掌握 Kotlin 类型参数化与错误处理机制,编写更安全、更灵活的代码
类型推理,穷举所有可能
亲爱的华生,一桩好案件的关键在于:先圈定嫌疑范围,再依据铁证逐一排除,直到只剩下唯一的真相。泛型就是这门推理术——类型参数 T 是嫌疑人档案,约束条件是筛选的证据链,而异常处理则是为每一条可能的死胡同提前布下的应急预案。今天,让我们以侦探的眼光,穷举类型系统与错误处理的一切可能。
华生,你是否注意到,调查案件时我们经常需要一套通用的推理框架——无论嫌疑人是商人、贵族还是仆从,推理的方法是相同的,变化的只是具体的人物。编程中也是如此:一段逻辑对多种类型都适用,如果为每种类型各写一份代码,不仅冗余而且难以维护。泛型(Generics)正是这套"通用推理框架"——它让我们在定义函数、类或接口时使用类型参数(即嫌疑人的"占位符"),在实际调用时再填入具体类型,从而同时获得类型安全和代码复用两大优势。
最基础的泛型用法,就像建立一份"万能嫌疑人档案"——在函数名前用尖括号声明类型参数 T,调用时再推理出具体类型:
fun <T> printItem(item: T) {
println("项目: $item")
}
// 调用时可以显式指定类型,也可以让编译器自动推断
printItem<String>("Hello") // 显式指定
printItem(42) // 编译器推断为 Int
泛型类是最常见的应用场景——想象一个"证物箱",它能安全地保管任何类型的物品,但取出时你总能确切知道里面是什么:
class Box<T>(val value: T) {
fun getContent(): T = value
fun describe(): String = "Box 中装的是: $value"
}
// 使用
val stringBox = Box("Kotlin") // Box<String>
val intBox = Box(2024) // Box<Int>
println(stringBox.getContent()) // "Kotlin"
println(intBox.describe()) // "Box 中装的是: 2024"
一桩案件往往涉及多位嫌疑人——当我们需要多个类型参数时,用逗号分隔即可:
class Pair<A, B>(
val first: A,
val second: B
) {
fun toFormattedString(): String = "($first, $second)"
}
val nameAge = Pair("张三", 25)
println(nameAge.toFormattedString()) // "(张三, 25)"
接口同样支持泛型。这就像制定一套标准调查流程——无论是调查人物、物品还是事件,操作步骤(查询、保存、删除)是统一的:
interface Repository<T> {
fun getById(id: Int): T?
fun getAll(): List<T>
fun save(item: T)
fun delete(item: T)
}
data class User(val id: Int, val name: String)
class UserRepository : Repository<User> {
private val users = mutableListOf<User>()
override fun getById(id: Int): User? = users.find { it.id == id }
override fun getAll(): List<User> = users.toList()
override fun save(item: User) { users.add(item) }
override fun delete(item: User) { users.remove(item) }
}
常用的类型参数名称:T(Type)、E(Element)、K(Key)、V(Value)、R(Result)。虽然可以使用任何名称,但遵循约定有助于代码可读性——正如侦探档案中的编号系统,统一命名才能高效检索。
Kotlin 泛型有类型擦除(运行时不知道 T 是什么),C++ 模板是编译期实例化(每个 T 生成独立代码)。Kotlin 的 List<String> 运行时只是 List,C++ 的 vector<string> 是完全独立的类型。换言之,Kotlin 在运行时"销毁了嫌疑人档案",而 C++ 为每位嫌疑人都建立了独立的卷宗。
华生,调查案件时我们不会漫无目的地排查全城居民——我们会根据证据缩小嫌疑范围。类型约束也是同理:通过上界约束(Upper Bound),我们限制类型参数必须是某个类型或其子类型,就像排除掉那些显然不可能的嫌疑人:
fun <T : Comparable<T>> findMax(a: T, b: T): T {
return if (a > b) a else b
}
println(findMax(10, 20)) // 20
println(findMax("apple", "banana")) // "banana"
由于约束了 T : Comparable<T>,编译器就掌握了一条铁证:传入的类型一定实现了比较接口。有了这条证据,使用 > 运算符就是完全安全的——我们已经排除了所有不具备"可比较"能力的类型。
有时一条线索不足以锁定嫌疑人,我们需要交叉比对多条证据。当类型参数需要同时满足多个约束时,使用 where 关键字——这就是联合多条证据链的推理:
fun <T> copyIfValid(item: T)
where T : Comparable<T>,
T : Cloneable {
println("该元素可比较且可克隆: $item")
}
// 泛型类同样可以使用 where
class SortedStorage<T>
where T : Comparable<T>,
T : Any {
private val items = mutableListOf<T>()
fun add(item: T) {
items.add(item)
items.sort()
}
fun getAll(): List<T> = items.toList()
}
现在来谈谈型变(Variance),这是泛型推理中最精妙的一环。华生,请思考:如果 Dog 是 Animal 的子类,那么 List<Dog> 能当作 List<Animal> 使用吗?这就像问:一份只记录了警犬的名单,能否作为动物名单使用?
答案取决于证据的流向。使用 out 关键字可以声明协变——即泛型类只"输出"(生产)该类型的值,不"输入"(消费)它。证据只从内部流向外部,如同线人只提供情报、不接受指令:
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
class Cat(name: String) : Animal(name)
// 使用 out 声明协变:只能读取 T,不能写入 T
interface Source<out T> {
fun next(): T // 可以:T 出现在返回值位置
// fun add(item: T) // 不可以:T 不能出现在参数位置
}
class DogSource : Source<Dog> {
override fun next(): Dog = Dog("旺财")
}
// 因为 Source 是协变的,所以 Source<Dog> 可以赋值给 Source<Animal>
val animalSource: Source<Animal> = DogSource()
println(animalSource.next().name) // "旺财"
Kotlin 标准库中的 List<out E> 就是协变的——它是只读列表,只产出元素,因此 List<Dog> 可以安全地作为 List<Animal> 使用。推理至此,铁证如山。
与协变相反,in 关键字声明逆变——泛型类只"消费"该类型的值。证据只从外部流入内部,如同法庭只接收证据、不对外发布:
// 使用 in 声明逆变:只能写入 T,不能读取 T
interface Consumer<in T> {
fun consume(item: T) // 可以:T 出现在参数位置
// fun produce(): T // 不可以:T 不能出现在返回值位置
}
class AnimalHandler : Consumer<Animal> {
override fun consume(item: Animal) {
println("处理动物: ${item.name}")
}
}
// 因为 Consumer 是逆变的,所以 Consumer<Animal> 可以赋值给 Consumer<Dog>
val dogHandler: Consumer<Dog> = AnimalHandler()
dogHandler.consume(Dog("小白")) // "处理动物: 小白"
推理一下便知:一个能处理所有 Animal 的处理器,当然也能处理 Dog——这就像苏格兰场的资深探长,既能办大案也能办小案。标准库中的 Comparator<in T> 就是逆变的典型例子。
| 关键字 | 名称 | T 的位置 | 子类型关系 | 类比 |
|---|---|---|---|---|
out |
协变 | 只能作为返回值(输出) | Dog : Animal => C<Dog> : C<Animal> | 生产者(Producer) |
in |
逆变 | 只能作为参数(输入) | Dog : Animal => C<Animal> : C<Dog> | 消费者(Consumer) |
| 无 | 不变 | 可作为参数和返回值 | C<Dog> 与 C<Animal> 无关 | MutableList<T> |
记忆口诀:Producer-Extends, Consumer-Super(对应 Kotlin 中的 out 和 in)。用侦探的话说:如果你只从线人那里获取情报,用 out;如果你只向档案室提交证据,用 in。
C++ 没有内建的型变标注(out/in),模板天然不支持协变和逆变。在 C++ 中如果需要类似效果,必须手动通过模板特化或 SFINAE 约束来处理。Kotlin 的声明处型变(declaration-site variance)比 Java 的使用处通配符(? extends / ? super)更简洁,而 C++ 则完全没有这层自动推理。
当你不关心具体的类型参数,或者不知道嫌疑人究竟是谁时——就像收到一个匿名线报,你只知道"有人"但不知道具体身份——可以使用星投影 *:
fun printListSize(list: List<*>) {
println("列表大小: ${list.size}")
// 可以安全读取为 Any?
for (item in list) {
println(" 元素: $item")
}
}
printListSize(listOf(1, 2, 3))
printListSize(listOf("a", "b"))
List<*> 等价于 List<out Any?>,即你可以安全地读取元素(类型为 Any?),但不能向其中写入任何东西——就像你可以观察匿名线人提供的物品,但无法向他交付任务。
华生,这是一桩令人头疼的"证据消失案"。Kotlin 运行在 JVM 上,而 JVM 的泛型采用类型擦除(Type Erasure)机制——在编译后,泛型的类型信息被抹去了。这意味着在运行时,我们无法推理出 T 究竟是谁——嫌疑人档案在进入法庭前被人销毁了:
// 以下代码无法编译!类型擦除导致运行时不知道 T 是什么
fun <T> isType(value: Any): Boolean {
return value is T // 编译错误:Cannot check for instance of erased type: T
}
但一位出色的侦探绝不会被证据的消失所难倒。Kotlin 提供了 inline 函数配合 reified 类型参数来破解此案。inline 函数在调用处内联展开,编译器在展开时将具体的类型信息"嵌入"到代码中——相当于在每个调查现场都保留了一份嫌疑人档案的副本:
inline fun <reified T> isType(value: Any): Boolean {
return value is T // 现在可以了!
}
println(isType<String>("hello")) // true
println(isType<Int>("hello")) // false
println(isType<Int>(42)) // true
reified 解决了类型擦除问题,效果类似 C++ 模板特化——在编译期为每个具体类型生成专用代码。但实现机制不同:C++ 模板在编译期实例化出完整的独立函数,Kotlin 的 reified 则通过 inline 内联展开来保留类型信息。C++ 天然没有类型擦除问题(typeid 和 dynamic_cast 始终可用),因此不需要类似的"补救"机制。
下面模拟标准库 filterIsInstance 的功能——从一堆混杂的证据中,精准筛选出某一类别的物证:
inline fun <reified T> List<Any>.filterByType(): List<T> {
val result = mutableListOf<T>()
for (item in this) {
if (item is T) {
result.add(item)
}
}
return result
}
val mixed = listOf(1, "hello", 3.14, "world", 42)
val strings = mixed.filterByType<String>() // ["hello", "world"]
val ints = mixed.filterByType<Int>() // [1, 42]
在 Android 开发中,reified 经常用于简化 Gson/Moshi 等 JSON 库的调用——让编译器自动推理目标类型,省去手动传递 Class 参数的繁琐:
import com.google.gson.Gson
inline fun <reified T> fromJson(json: String): T {
return Gson().fromJson(json, T::class.java)
}
// 使用:无需手动传递 Class 参数
data class Product(val name: String, val price: Double)
val json = """{"name":"手机","price":4999.0}"""
val product: Product = fromJson(json)
println(product.name) // "手机"
reified 只能用在 inline 函数中,不能用在类的类型参数上。此外,由于内联会导致代码膨胀,不宜在过长的函数上使用——就像不能在每份案卷里都完整复印整个档案库。
华生,在真实的调查工作中,每次行动的结果无非三种:成功获取线索、遭遇失败、仍在进行中。Android 网络请求也是如此。用密封类配合泛型,我们可以穷举所有可能的结果,不遗漏任何一条分支——这正是侦探精神的体现:
sealed class ApiResult<out T> {
data class Success<T>(val data: T) : ApiResult<T>()
data class Error(val message: String, val code: Int) : ApiResult<Nothing>()
object Loading : ApiResult<Nothing>()
}
// 在 ViewModel 中使用
class UserViewModel {
suspend fun fetchUser(id: Int): ApiResult<User> {
return try {
val user = User(id, "用户$id")
ApiResult.Success(user)
} catch (e: Exception) {
ApiResult.Error(e.message ?: "未知错误", 500)
}
}
}
// 调用处使用 when 进行模式匹配
fun handleResult(result: ApiResult<User>) {
when (result) {
is ApiResult.Success -> println("用户: ${result.data.name}")
is ApiResult.Error -> println("错误: ${result.message}")
is ApiResult.Loading -> println("加载中...")
}
}
在 Android 中,我们经常需要为不同的数据类型编写相似的列表适配器。利用泛型抽取公共逻辑,就像建立一套标准化的调查程序——无论调查对象是谁,流程保持一致:
abstract class BaseAdapter<T, VH : RecyclerView.ViewHolder> :
RecyclerView.Adapter<VH>() {
protected val items = mutableListOf<T>()
fun submitList(newItems: List<T>) {
items.clear()
items.addAll(newItems)
notifyDataSetChanged()
}
override fun getItemCount(): Int = items.size
fun getItem(position: Int): T = items[position]
}
泛型扩展函数是 Kotlin 的一大利器——可以为已有类型添加类型安全的通用方法。就像一位侦探可以随时为调查工具箱增添新工具,而不必改造工具箱本身:
// 安全类型转换
inline fun <reified T> Any.safeCast(): T? = this as? T
// 非空时执行操作并返回结果
fun <T, R> T?.whenNotNull(block: (T) -> R): R? {
return if (this != null) block(this) else null
}
// 列表安全获取
fun <T> List<T>.safeGet(index: Int, default: T): T {
return if (index in indices) this[index] else default
}
// 使用示例
val result = "hello".safeCast<String>() // "hello"
val number = "hello".safeCast<Int>() // null
val name: String? = "Kotlin"
name.whenNotNull { println("名称: $it") } // "名称: Kotlin"
val list = listOf(10, 20, 30)
println(list.safeGet(5, 0)) // 0(越界时返回默认值)
华生,即便是最周密的调查计划,也无法预见所有突发状况:线人失联、证据被篡改、通讯中断……编程同理,运行时总会遇到不可预见的情况:网络断开、文件不存在、数据格式错误等。异常处理机制就是我们为每一桩案件预先制定的应急预案——确保即使遭遇意外,调查(程序)也不会彻底崩溃。
Kotlin 的异常处理使用 try、catch 和 finally 三个关键字——分别对应"尝试执行调查"、"遭遇意外时的应对"和"无论结果如何都要执行的收尾工作":
fun readNumber(input: String): Int {
try {
return input.toInt()
} catch (e: NumberFormatException) {
println("无法将 '$input' 转换为数字: ${e.message}")
return 0
} finally {
println("转换操作完成")
}
}
println(readNumber("42")) // 42
println(readNumber("abc")) // 0
finally 块中的代码无论是否发生异常都会执行——这是案件收尾的铁律,无论调查成功与否,现场都必须恢复原状。
与 Java 不同,Kotlin 中的 try 是一个表达式,可以直接将其结果赋给变量。这意味着我们的调查结论可以直接写入案卷,无需迂回:
val number = try {
"123".toInt()
} catch (e: NumberFormatException) {
0
}
println(number) // 123
val fallback = try {
"abc".toInt()
} catch (e: NumberFormatException) {
-1
}
println(fallback) // -1
Kotlin 无受检异常,所有异常都像 C++ 的运行时异常;C++ 有 noexcept 说明符来标注函数不抛异常,Kotlin 没有对应机制。关键区别:Kotlin 的 try 是表达式(有返回值,可以赋给变量),C++ 的 try 是语句(没有返回值)。这让 Kotlin 的错误处理可以写得更简洁。
一桩复杂案件可能面临多种不同类型的意外——我们需要针对每种情况分别制定应对策略。可以使用多个 catch 块来穷举各种异常类型:
fun processData(data: String, index: Int): Char {
return try {
val number = data.toInt()
val text = "结果是 $number"
text[index]
} catch (e: NumberFormatException) {
println("格式错误: ${e.message}")
'?'
} catch (e: IndexOutOfBoundsException) {
println("索引越界: $index")
'!'
} catch (e: Exception) {
println("其他异常: ${e.message}")
'#'
}
}
多个 catch 块时,应将更具体的异常类型放在前面,更通用的放在后面。如果把 Exception 放在第一个,后面的 catch 块将永远不会被执行——这就像在排除嫌疑人时,如果一开始就说"所有人都有嫌疑",那后续的精细筛查便毫无意义。
与 Java 不同,Kotlin 没有受检异常(Checked Exceptions)。Kotlin 的设计者经过审慎推理,认为受检异常在实践中弊大于利:
在 Kotlin 中,所有异常都是非受检的。如果需要与 Java 互操作并声明异常,可以使用 @Throws 注解:
@Throws(IOException::class)
fun readFile(path: String): String {
// Java 代码调用此函数时,编译器会强制处理 IOException
return File(path).readText()
}
华生,当标准的报案类型无法准确描述案情时,我们就需要创建专属的案件分类。同理,当标准异常类型不足以描述业务错误时,应创建自定义异常类——为每种"案件类型"建立独立的档案:
// 业务异常基类
open class BusinessException(
message: String,
val errorCode: Int
) : Exception(message)
// 具体异常类
class UserNotFoundException(
userId: Int
) : BusinessException("用户 $userId 不存在", 404)
class InsufficientBalanceException(
required: Double,
available: Double
) : BusinessException(
"余额不足: 需要 $required,当前 $available", 402
)
// 使用示例
fun withdraw(amount: Double, balance: Double): Double {
if (amount > balance) {
throw InsufficientBalanceException(amount, balance)
}
return balance - amount
}
try {
withdraw(1000.0, 500.0)
} catch (e: InsufficientBalanceException) {
println("[${e.errorCode}] ${e.message}")
// [402] 余额不足: 需要 1000.0,当前 500.0
}
Nothing 是 Kotlin 中一个特殊的类型,表示"永远不会有返回值"——就像一条必然导致案件终结的线索:一旦走上这条路,就不可能再回头。当一个函数总是抛出异常或者是一个无限循环时,可以将其返回类型声明为 Nothing:
fun fail(message: String): Nothing {
throw IllegalStateException(message)
}
// Nothing 的好处:编译器知道此函数永远不会正常返回
fun getUserName(user: User?): String {
val name = user?.name ?: fail("用户不能为空")
// 编译器在此处确定 name 一定是 String(非空),
// 因为如果 user?.name 为 null,fail() 会抛出异常
return name
}
Kotlin 标准库提供了一组前置条件函数,如同侦探出发前的安全检查清单——在不满足条件时快速抛出异常,将问题扼杀在萌芽阶段:
fun createUser(name: String, age: Int): User {
// require —— 验证参数,不满足时抛出 IllegalArgumentException
require(name.isNotBlank()) { "用户名不能为空" }
require(age in 0..150) { "年龄必须在 0 到 150 之间,当前: $age" }
return User(0, name)
}
class Connection {
var isConnected = false
fun sendData(data: String) {
// check —— 验证对象状态,不满足时抛出 IllegalStateException
check(isConnected) { "连接未建立,无法发送数据" }
println("发送数据: $data")
}
}
// error —— 直接抛出 IllegalStateException
fun getStatus(code: Int): String = when (code) {
200 -> "成功"
404 -> "未找到"
500 -> "服务器错误"
else -> error("未知状态码: $code")
}
| 函数 | 用途 | 抛出的异常 |
|---|---|---|
require(condition) |
验证函数参数 | IllegalArgumentException |
requireNotNull(value) |
验证参数非空 | IllegalArgumentException |
check(condition) |
验证对象状态 | IllegalStateException |
checkNotNull(value) |
验证状态值非空 | IllegalStateException |
error(message) |
直接抛出异常并终止 | IllegalStateException |
推理原则很简单:验证外部输入(函数参数,即"证人证词")用 require,验证内部状态(对象自身状态,即"调查进度")用 check,标记"不应该到达的代码"(即"推理中的不可能分支")用 error。
华生,传统的 try-catch 就像在案件现场亲自处置每一个突发状况——有效但不够优雅。Kotlin 标准库提供了 runCatching 函数和 Result<T> 类型,这是一种更精密的方法——将每次调查的结论封装为一个案件裁决(Result),无论成功还是失败,都以统一的形式呈堂。这种函数式的处理方式更适合链式推理和证据传递。
runCatching 会执行给定的代码块,如果成功则返回 Result.success(value)(案件告破),如果抛出异常则返回 Result.failure(exception)(调查受阻):
val result = runCatching {
"42".toInt()
}
println(result.isSuccess) // true
println(result.getOrNull()) // 42
val failed = runCatching {
"abc".toInt()
}
println(failed.isFailure) // true
println(failed.getOrNull()) // null
Result<T> 提供了多种处理成功和失败结果的方法——就像侦探针对不同的案件裁决,可以选择不同的后续行动方案:
val result = runCatching {
fetchDataFromNetwork()
}
// getOrDefault:失败时返回默认值
val data1 = result.getOrDefault("默认数据")
// getOrElse:失败时通过 lambda 计算返回值
val data2 = result.getOrElse { exception ->
println("请求失败: ${exception.message}")
"备用数据"
}
// onSuccess / onFailure:分别在成功或失败时执行操作
result
.onSuccess { data -> println("获取成功: $data") }
.onFailure { error -> println("获取失败: ${error.message}") }
Kotlin 的 Result<T> 类似 C++23 的 std::expected<T, E>,两者都将成功值或错误封装在同一个对象中。但 Kotlin 的 Result 更函数式——内置 map、fold、recover 等链式操作,而 std::expected 的链式 API 相对有限(and_then、transform)。此外,Kotlin 的 Result 固定以 Throwable 作为错误类型,C++ 的 std::expected 则允许自定义错误类型 E。
Result 支持 map、fold 等链式操作,使得错误处理可以像推理链条一样层层递进——每一步的结论成为下一步推理的输入:
fun parseAge(input: String): String {
return runCatching {
input.trim().toInt()
}.map { age ->
// 成功时转换结果
when {
age < 18 -> "未成年"
age < 60 -> "成年人"
else -> "老年人"
}
}.getOrDefault("输入无效")
}
println(parseAge("25")) // "成年人"
println(parseAge("abc")) // "输入无效"
fold 方法接受两个 lambda,分别处理成功和失败的情况,并返回统一类型的结果——穷举了所有可能的案件走向,不留任何死角:
fun loadUserProfile(userId: Int): String {
return runCatching {
// 模拟可能失败的操作
if (userId <= 0) throw IllegalArgumentException("无效 ID")
User(userId, "用户$userId")
}.fold(
onSuccess = { user -> "欢迎, ${user.name}!" },
onFailure = { error -> "加载失败: ${error.message}" }
)
}
println(loadUserProfile(1)) // "欢迎, 用户1!"
println(loadUserProfile(-1)) // "加载失败: 无效 ID"
runCatching 也可以作为扩展函数在对象上调用,此时 lambda 内的 this 指向该对象——就像以某个特定嫌疑人为切入点展开调查:
val input = " 123 "
val result = input.runCatching {
trim().toInt() // this 就是 input 字符串
}
println(result.getOrNull()) // 123
| 特性 | try-catch | runCatching |
|---|---|---|
| 代码风格 | 命令式 | 函数式 |
| 链式调用 | 不支持 | 支持(map, fold, onSuccess 等) |
| 结果传递 | 通过变量或 return | 通过 Result<T> 对象 |
| 多异常类型 | 多个 catch 块分别处理 | 在 onFailure/fold 中统一处理 |
| finally 支持 | 原生支持 | 无直接支持,但可使用 also |
| 适用场景 | 复杂的分类错误处理、需要 finally | 结果转换、链式处理、传递错误 |
在实际项目中,两种方式经常结合使用。下面是一个综合示例,展示如何用泛型和异常处理构建一个健壮的数据加载工具——穷举所有成功与失败的可能性,确保推理链条的每一环都无懈可击:
sealed class LoadResult<out T> {
data class Success<T>(val data: T) : LoadResult<T>()
data class Failure(val error: Throwable) : LoadResult<Nothing>()
}
// 通用的安全加载函数
inline fun <T> safeLoad(block: () -> T): LoadResult<T> {
return runCatching { block() }
.fold(
onSuccess = { LoadResult.Success(it) },
onFailure = { LoadResult.Failure(it) }
)
}
// 使用
fun main() {
val userResult = safeLoad {
// 模拟网络请求
User(1, "张三")
}
when (userResult) {
is LoadResult.Success -> {
println("加载成功: ${userResult.data.name}")
}
is LoadResult.Failure -> {
println("加载失败: ${userResult.error.message}")
}
}
// 演示多步骤链式处理
val displayName = runCatching { fetchUserFromApi(1) }
.map { user -> user.name.uppercase() }
.recover { "匿名用户" }
.getOrDefault("未知")
println("显示名称: $displayName")
}
对于简单的局部错误处理,try-catch 如同现场即时决断,清晰直接;对于需要传递错误、链式转换或函数式风格的场景,runCatching + Result 如同将案件裁决书逐级上报,更为系统化。在 Android 的 Repository 层和 ViewModel 层,runCatching 配合密封类是当下最流行的推理模式。
实现一个泛型类 Stack<T>,支持 push(item: T)、pop(): T?、peek(): T? 和 isEmpty(): Boolean 四个操作。
class Stack<T> {
private val elements = mutableListOf<T>()
fun push(item: T) {
elements.add(item)
}
fun pop(): T? = if (elements.isNotEmpty()) {
elements.removeAt(elements.size - 1)
} else null
fun peek(): T? = elements.lastOrNull()
fun isEmpty(): Boolean = elements.isEmpty()
}
// 测试
val stack = Stack<Int>()
stack.push(1)
stack.push(2)
stack.push(3)
println(stack.peek()) // 3
println(stack.pop()) // 3
println(stack.pop()) // 2
编写一个泛型函数 sortedBetween,接受一个 List<T> 和上下界 min、max,返回列表中值在 [min, max] 范围内的元素并排序。要求 T 必须实现 Comparable<T>。
fun <T : Comparable<T>> sortedBetween(
list: List<T>,
min: T,
max: T
): List<T> {
require(min <= max) { "min 不能大于 max" }
return list.filter { it in min..max }.sorted()
}
// 测试
val numbers = listOf(5, 3, 8, 1, 9, 2, 7)
println(sortedBetween(numbers, 3, 8)) // [3, 5, 7, 8]
val words = listOf("dog", "cat", "bird", "elephant")
println(sortedBetween(words, "c", "e")) // [cat, dog]
编写一个 inline + reified 的扩展函数 castOrDefault,将 Any 安全转换为目标类型 T,转换失败时返回给定的默认值。
inline fun <reified T> Any.castOrDefault(default: T): T {
return if (this is T) this else default
}
// 测试
val a: Any = "Hello"
println(a.castOrDefault<String>("默认")) // "Hello"
println(a.castOrDefault<Int>(0)) // 0
val b: Any = 42
println(b.castOrDefault<Int>(0)) // 42
println(b.castOrDefault<String>("默认")) // "默认"
编写一个函数 parseConfig,接受一个 JSON 格式的字符串(如 "port:8080"),使用 runCatching 链式操作来:(1) 以 : 分割字符串,(2) 取第二部分转为 Int,(3) 验证端口范围在 1~65535 之间。任何一步失败都返回默认端口 8080。
fun parseConfig(config: String): Int {
return runCatching {
val parts = config.split(":")
require(parts.size == 2) { "格式错误" }
parts[1].trim().toInt()
}.map { port ->
require(port in 1..65535) { "端口越界" }
port
}.getOrDefault(8080)
}
// 测试
println(parseConfig("port:3000")) // 3000
println(parseConfig("port:abc")) // 8080
println(parseConfig("port:99999")) // 8080
println(parseConfig("invalid")) // 8080
设计一个事件系统:(1) 定义基类 Event 及其子类 ClickEvent 和 KeyEvent;(2) 定义协变接口 EventProducer<out T : Event> 和逆变接口 EventConsumer<in T : Event>;(3) 实现具体的 ClickProducer 和 AnyEventLogger(消费所有 Event);(4) 演示协变赋值(EventProducer<ClickEvent> 赋给 EventProducer<Event>)和逆变赋值(EventConsumer<Event> 赋给 EventConsumer<ClickEvent>)。
open class Event(val timestamp: Long = System.currentTimeMillis())
class ClickEvent(val x: Int, val y: Int) : Event()
class KeyEvent(val keyCode: Int) : Event()
// 协变:只产出事件
interface EventProducer<out T : Event> {
fun produce(): T
}
// 逆变:只消费事件
interface EventConsumer<in T : Event> {
fun consume(event: T)
}
class ClickProducer : EventProducer<ClickEvent> {
override fun produce(): ClickEvent = ClickEvent(100, 200)
}
class AnyEventLogger : EventConsumer<Event> {
override fun consume(event: Event) {
println("事件记录 @ ${event.timestamp}")
}
}
// 协变演示:ClickEvent 是 Event 的子类,
// 所以 EventProducer<ClickEvent> 可以赋给 EventProducer<Event>
val producer: EventProducer<Event> = ClickProducer()
val event = producer.produce()
// 逆变演示:EventConsumer<Event> 可以赋给 EventConsumer<ClickEvent>
val clickConsumer: EventConsumer<ClickEvent> = AnyEventLogger()
clickConsumer.consume(ClickEvent(50, 75))