Kotlin中的泛型是一种允许类型参数化的特性,能够显著增强代码的复用性和类型安全性。在Kotlin中,我们使用尖括号< >
来声明泛型类型参数,这些参数可以在类或函数的成员中使用。
泛型类定义示例:
class Box<T>(t: T) {
var value = t
}
这里,T
是一个类型参数,表示Box
可以持有任何类型的值。创建实例时,我们可以指定具体的类型:
val intBox = Box(10) // 推断为Box
val stringBox = Box<String>("Hello")
泛型函数定义示例:
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
调用泛型函数时,Kotlin的类型推断机制通常能自动推导出类型参数:
val list = singletonList(1) // 返回 List
泛型约束:有时我们需要限制泛型类型必须满足某些条件,比如实现特定接口或继承某个类。这可以通过在类型参数后添加约束来实现:
fun <T : comparable<T>> sort(list: List<T>) {
// 排序实现
}
在这个例子中,类型参数T
必须是comparable
的子类,确保我们可以安全地对元素进行比较。
Kotlin的泛型型变机制允许我们更灵活地使用泛型类型,分为协变(covariance)和逆变(contravariance)两种。
协变(out):当泛型类型参数被声明为out
时,表示该类型参数只能出现在输出位置,即只能被读取而不能被写入。这使得Cache
可以安全地接受Cache
实例,因为Int
是Number
的子类,而协变保证我们只能获取Number
类型的值:
class Producer<out T>(private val value: T) {
fun produce(): T {
return value
}
}
val producer: Producer<Number> = Producer(10) // 合法
逆变(in):逆变则相反,当泛型类型参数被声明为in
时,表示该类型参数只能出现在输入位置,即只能被写入而不能被读取。这使得Cache
可以安全地接受Cache
实例,因为Number
是Byte
的父类,而逆变保证我们只能输入Number
类型的值:
class Consumer<in T> {
fun consume(item: T) {
// 处理输入项
}
}
val consumer: Consumer<Number> = Consumer<Byte>() // 合法
类型投影:Kotlin还支持使用通配符*
进行类型投影,允许我们动态地改变泛型类型的协变或逆变特性:
fun copy(from: Array<out Any>, to: Array<in Any>) {
// 从from复制到to
}
在使用点变化(Use-site variance)中,out
表示协变,只能读取;in
表示逆变,只能写入。这种机制使得Kotlin的泛型比Java更加直观和安全。
Kotlin和Java一样,使用类型擦除机制实现泛型,这意味着在运行时,泛型类型信息会被擦除。然而,Kotlin提供了一个特殊的关键字reified
,允许我们在某些情况下保留泛型类型信息。
泛型实化示例:
inline fun <reified T> getGenericType() = T::class.java
fun main() {
val r = getGenericType<String>() // 返回 class java.lang.String
val m = getGenericType<Int>() // 返回 class java.lang.Integer
}
reified
关键字只能用于内联函数中,它允许我们访问泛型参数的运行时类型信息。这种特性在工厂模式、类型检查等场景中非常有用,但需要注意的是,使用reified
的内联函数无法在Java代码中调用。
键值对缓存是一种常见的缓存模式,它通过键值对的形式存储数据,提供快速的查找和访问机制。在Kotlin中,我们可以利用泛型实现一个灵活且类型安全的缓存系统。
键值对缓存模式分类:
键值对缓存可以归类为策略模式和组合模式的结合应用:
缓存策略对比:
策略 | 核心原理 | 适用场景 | 优缺点 |
---|---|---|---|
LRU | 最近最少使用 | 热点数据集中、内存敏感型系统 | 实现简单,适合热点数据;无法处理访问频率差异大的数据 |
TTL | 设置生存时间 | 临时性数据、需要严格时效性的场景 | 强制数据过期,避免脏数据;无法利用访问模式优化缓存利用率 |
LFU | 最不频繁使用 | 访问模式稳定、长尾效应明显的场景 | 保留高频访问数据;实现复杂,维护计数器开销大 |
FIFO | 先进先出 | 简单场景、无明显热点数据 | 实现极其简单;无视访问模式,可能淘汰仍有价值的数据 |
永久性缓存是指没有过期机制的缓存,数据会一直保留直到被显式移除。这种缓存适合存储不太可能改变或可以长期保持有效性的数据。
手动实现永久性缓存:
class永久性缓存<T> {
private val map = mutableMapOf<String, T>()
private val锁 = Any()
fun put(key: String, value: T) {
synchronized(锁) {
map[key] = value
}
}
fun get(key: String): T? {
synchronized(锁) {
return map[key]
}
}
fun remove(key: String): T? {
synchronized(锁) {
return map.remove(key)
}
}
fun clear() {
synchronized(锁) {
map.clear()
}
}
}
线程安全优化:在多线程环境中,我们需要确保缓存操作的线程安全。可以通过ConcurrentHashMap
来实现:
import java.util.concurrent.ConcurrentHashMap
class线程安全缓存<K, V> {
private val map = ConcurrentHashMap<K, V>()
private val锁 = Any()
fun put(key: K, value: V) {
synchronized(锁) {
map[key] = value
}
}
fun get(key: K): V? {
synchronized(锁) {
return map[key]
}
}
fun remove(key: K): V? {
synchronized(锁) {
return map.remove(key)
}
}
fun clear() {
synchronized(锁) {
map.clear()
}
}
}
永久性缓存的局限性:永久性缓存的主要问题是容量控制,当缓存数据量超过内存限制时,会导致性能下降甚至内存溢出。因此,在实际应用中,我们需要结合容量限制策略。
基于时间的缓存是指为每个缓存项设置一个过期时间,当时间到达时,自动从缓存中移除。这种缓存特别适合需要保持数据时效性的场景。
实现TTL缓存:
data class带时间戳的值<V>(val value: V, val expiresAt: Instant)
class基于时间的缓存<K, V> {
private val map = mutableMapOf<K, 带时间戳的值<V>>()
private val锁 =