一、泛型基础知识与型变机制
1.1 Kotlin泛型基本语法与类型参数
Kotlin中的泛型是一种允许类型参数化的特性,能够显著增强代码的复用性和类型安全性。在Kotlin中,我们使用尖括号< >
来声明泛型类型参数,这些参数可以在类或函数的成员中使用。
泛型类定义示例:
class Box<T>(t: T) {
var value = t
}
这里,T
是一个类型参数,表示Box
可以持有任何类型的值。创建实例时,我们可以指定具体的类型:
val intBox = Box(10) // 推断为Box<Int>
val stringBox = Box<String>("Hello")
泛型函数定义示例:
fun <T> singletonList(item: T): List<T> {
return listOf(item)
}
调用泛型函数时,Kotlin的类型推断机制通常能自动推导出类型参数:
val list = singletonList(1) // 返回 List<Int>
泛型约束:有时我们需要限制泛型类型必须满足某些条件,比如实现特定接口或继承某个类。这可以通过在类型参数后添加约束来实现:
fun <T : comparable<T>> sort(list: List<T>) {
// 排序实现
}
在这个例子中,类型参数T
必须是comparable<T>
的子类,确保我们可以安全地对元素进行比较。
1.2 泛型型变:协变与逆变
Kotlin的泛型型变机制允许我们更灵活地使用泛型类型,分为协变(covariance)和逆变(contravariance)两种。
协变(out):当泛型类型参数被声明为out
时,表示该类型参数只能出现在输出位置,即只能被读取而不能被写入。这使得Cache<Number>
可以安全地接受Cache<Int>
实例,因为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<Number>
可以安全地接受Cache<Byte>
实例,因为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更加直观和安全。
1.3 泛型实化(reified)与运行时类型信息
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代码中调用。
二、键值对缓存设计模式与实现策略
2.1 缓存设计模式概述
键值对缓存是一种常见的缓存模式,它通过键值对的形式存储数据,提供快速的查找和访问机制。在Kotlin中,我们可以利用泛型实现一个灵活且类型安全的缓存系统。
键值对缓存模式分类:
键值对缓存可以归类为策略模式和组合模式的结合应用:
策略模式:通过不同的淘汰策略(如LRU、LFU、TTL)实现不同的缓存行为。
组合模式:将缓存层与数据源组合,提供统一的访问接口。
缓存策略对比:
策略 | 核心原理 | 适用场景 | 优缺点 |
---|---|---|---|
LRU | 最近最少使用 | 热点数据集中、内存敏感型系统 | 实现简单,适合热点数据;无法处理访问频率差异大的数据 |
TTL | 设置生存时间 | 临时性数据、需要严格时效性的场景 | 强制数据过期,避免脏数据;无法利用访问模式优化缓存利用率 |
LFU | 最不频繁使用 | 访问模式稳定、长尾效应明显的场景 | 保留高频访问数据;实现复杂,维护计数器开销大 |
FIFO | 先进先出 | 简单场景、无明显热点数据 | 实现极其简单;无视访问模式,可能淘汰仍有价值的数据 |
2.2 永久性缓存实现策略
永久性缓存是指没有过期机制的缓存,数据会一直保留直到被显式移除。这种缓存适合存储不太可能改变或可以长期保持有效性的数据。
手动实现永久性缓存:
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()
}
}
}
永久性缓存的局限性:永久性缓存的主要问题是容量控制,当缓存数据量超过内存限制时,会导致性能下降甚至内存溢出。因此,在实际应用中,我们需要结合容量限制策略。
2.3 基于时间的缓存实现策略
基于时间的缓存是指为每个缓存项设置一个过期时间,当时间到达时,自动从缓存中移除。这种缓存特别适合需要保持数据时效性的场景。
实现TTL缓存:
data class带时间戳的值<V>(val value: V, val expiresAt: Instant)
class基于时间的缓存<K, V> {
private val map = mutableMapOf<K, 带时间戳的值<V>>()
private val锁 = Any()
fun put(key: K, value: V, ttl: Duration) {
synchronized(锁) {
val expiresAt = Instant.now().plus(ttl)
map[key] = 带时间戳的值(value, expiresAt)
}
}
fun get(key: K): V? {
synchronized(锁) {
val entry = map[key]
return if (entry?.expiresAt!! > Instant.now()) entry.value else null
}
}
fun remove(key: K): V? {
synchronized(锁) {
return map.remove(key)?.value
}
}
fun clear() {
synchronized(锁) {
map.clear()
}
}
fun自动清理过期项() {
synchronized(锁) {
map.values.removeIf {
it expiresAt <= Instant.now() }
}
}
}
定期清理过期项:在实际应用中,我们可以使用定时任务定期清理过期项:
fun自动清理过期项(间隔: Duration) {
val调度器 = Executors.newScheduledThreadPool(1)
调度器.scheduleAtFixedRate({
synchronized(锁) {
map.values.removeIf {
it expiresAt <= Instant.now() }
}
}, 0, 间隔.toMillis(), TimeUnit.MILLISECONDS)
}
基于时间的缓存优势:基于时间的缓存确保数据不会无限期地存在于缓存中,避免了因数据源变化而产生的脏数据问题。然而,它的一个缺点是无法根据数据访问模式优化缓存利用率。
三、从零实现一个完整的泛型Cache类
3.1 基础版泛型Cache实现
现在我们开始手动实现一个完整的泛型Cache类。首先,实现一个基础版,使用mutableMapOf
作为存储结构:
class Cache<K, V> {
private val map = mutableMapOf<K, V>() // 使用可变映射作为存储
fun put(key: K, value: V) {
map[key] = value
}
fun get(key: K): V? {
return map[key]
}
fun remove(key: K): V? {
return map.remove(key)
}
fun clear() {
map.clear()
}
// 添加大小查询方法
fun size(): Int {
return map.size
}
// 添加是否包含键的方法
fun containsKey(key: K): Boolean {
return map.containsKey(key)
}
// 添加是否包含值的方法
fun containsValue(value: V): Boolean {
return map.containsValue(value)
}
// 添加获取所有键的方法
fun keys(): Set<K> {
return map.keys
}
// 添加获取所有值的方法
fun values(): Collection<V> {
return map.values
}
// 添加获取所有键值对的方法
fun entries(): Set<Map.Entry<K, V>> {
return map.entries
}
}
使用示例:
fun main() {
val cache = Cache<String, Int>() // 创建字符串到整数的缓存
cache.put("key1", 1) // 存入数据
cache.put("key2", 2)
println(cache.get("key1")) // 输出 1
println(cache.get("key2")) // 输出 2
cache.remove("key1") // 移除数据
println(cache.get("key1")) // 输出 null
cache.clear() // 清空缓存
println(cache.get("key2")) // 输出 null
}
基础版缓存的局限性:这个基础实现虽然简单,但在多线程环境中存在安全问题,且没有实现过期策略和容量控制。我们需要进一步优化。
3.2 线程安全版Cache实现
为解决多线程环境下的安全问题,我们可以使用synchronized
关键字或ConcurrentHashMap
来实现线程安全的缓存。
使用synchronized关键字的线程安全缓存:
class线程安全缓存<K, V> {
private val map = mutableMapOf<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()
}
}
}
使用ConcurrentHashMap的线程安全缓存:
import java.util.concurrent.ConcurrentHashMap
class线程安全缓存 Concurrency<K, V> {
private val map = ConcurrentHashMap<K, V>() // 使用线程安全映射
fun put(key: K, value: V) {
map[key] = value
}
fun get(key: K): V? {
return map[key]
}
fun remove(key: K): V? {
return map.remove(key)
}
fun clear() {
map.clear()
}
}
线程安全优化的对比:
使用synchronized
关键字需要显式加锁,代码更复杂,但可以更精确地控制锁的范围。
使用ConcurrentHashMap
则更简洁,内部已经处理了所有线程安全问题,但灵活性较低。
3.3 添加过期策略的Cache实现
为增强缓存的功能性,我们添加**时间到存活(TTL)和时间到闲置(TTI)**过期策略。
带过期时间的缓存实现:
data class带时间戳的值<V>(val value: V, val expiresAt: Instant)
class过期缓存<K, V> {
private val map = mutableMapOf<K, 带时间戳的值<V>>() // 存储值和过期时间
private val锁 = Any()
// TTL过期策略
fun putWithTTL(key: K, value: V, ttl: Duration) {
synchronized(锁) {
val expiresAt = Instant.now().plus(ttl)
map[key] = 带时间戳的值(value, expiresAt)
}
}
// TTI过期策略
fun putWithTTI(key: K, value: V, tti: Duration) {
synchronized(锁) {
val expiresAt = Instant.now().plus(tti)
map[key] = 带时间戳的值(value, expiresAt)
}
}
fun get(key: K): V? {
synchronized(锁) {
val entry = map[key]
return if (entry != null && entry expiresAt > Instant.now()) entry.value else null
}
}
fun自动清理过期项() {
synchronized(锁) {
map.values.removeIf {
it expiresAt <= Instant.now() }
}
}
// 其他方法与基础版相同
}
过期策略的实现原理:在put
方法中记录过期时间,在get
方法中惰性检查是否过期。这种方法不会立即移除过期项,而是在访问时检查,可以减少资源消耗,但可能导致获取到已过期的项。
3.4 添加容量控制的Cache实现
为防止缓存无限增长,我们实现**最近最少使用(LRU)**淘汰策略。
手动实现LRU缓存:
class手动LRU缓存<K, V> {
private val map = mutableMapOf<K, V>() // 存储键值对
private val访问顺序链表 = linkedHashMap<K, V>() // 维护访问顺序
private val锁 = Any()
fun put(key: K, value: V, capacity: Int) {
synchronized(锁) {
// 更新或添加新项到链表和映射
访问顺序链表[key] = value
map[key] = value
// 当超过容量时,移除最旧的项
while (访问顺序链表.size > capacity) {
val最旧键 = 访问顺序链表.keys扉页().next()
访问顺序链表.remove(最旧键)
map.remove(最旧键)
}
}
}
fun get(key: K): V? {
synchronized(锁) {
val value = map[key]
if (value != null) {
// 移除并重新添加到链表尾部,表示最近访问
访问顺序链表.remove(key)
访问顺序链表[key] = value
}
return value
}
}
// 其他方法与基础版相同
}
使用ConcurrentLinkedHashMap的LRU缓存:
import com.googlecode.concurrentlinkedhashmapConcurrentLinkedHashMap
import com.googlecode.concurrentlinkedhashmapEvictionListener
class线程安全LRU缓存<K, V> {
private val map = ConcurrentLinkedHashMap.Builder<K, V>()
.maximumWeightedCapacity(100) // 设置最大容量
.accessOrder(true) // 设置按访问顺序维护
.build()
fun put(key: K, value: V) {
map[key] = value
}
fun get(key: K): V? {
return map[key]
}
fun remove(key: K): V? {
return map.remove(key)
}
fun clear() {
map.clear()
}
// 添加淘汰监听器
fun添加淘汰监听器(监听器: EvictionListener<K, V>) {
map蒸发监听器(监听器)
}
}
ConcurrentLinkedHashMap的优势:这个库专门用于实现线程安全的LRU缓存,内部已经处理了所有并发问题,并且提供了更高效的实现。
3.5 完整泛型Cache类实现
现在我们将所有功能整合到一个完整的泛型Cache类中:
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedHashMap
import java.util.concurrent.locks.ReentrantLock
import java.time.Instant
import java.time.Duration
import java.util.concurrent.TimeUnit
class CompleteGenericCache<K, V> {
private val lock = ReentrantLock()
// 使用ConcurrentHashMap存储键值对和过期时间
private val map = ConcurrentHashMap<K, TimestampedValue<V>>() // 存储值和过期时间
// 使用ConcurrentLinkedHashMap维护访问顺序
private val accessOrderMap = ConcurrentLinkedHashMap.Builder<K, V>()
.maximumWeightedCapacity(100) // 默认容量
.accessOrder(true) // 按访问顺序维护
.build()
// 使用自定义Node类实现LRU
class Node internal constructor(key: K, value: V) {
var key: K = key
var value: V = value
var before: Node? = null
var after: Node? = null
}
private val manualLinkedList: MutableMap<K, Node> = mutableMapOf()
private var head: Node? = null
private var tail: Node? = null
// 初始化容量
private var capacity: Int = 100
// 构造函数
constructor(maxSize: Int) {
this.capacity = maxSize
accessOrderMap = ConcurrentLinkedHashMap.Builder<K, V>()
.maximumWeightedCapacity(maxSize)
.accessOrder(true)
.build()
}
// TTL过期策略
fun putWithTTL(key: K, value: V, ttl: Duration) {
lock.lock()
try {
val expiresAt = Instant.now().plus(ttl)
map[key] = TimestampedValue(value, expiresAt)
accessOrderMap[key] = value
// 手动维护访问顺序链表
val node = Node(key, value)
if (manualLinkedList.size == capacity) {
// 移除最旧的项
val oldestKey = head?.key
if (oldestKey != null) {
manualLinkedList.remove(oldestKey)
accessOrderMap.remove(oldestKey)
}
}
// 将新节点添加到链表尾部
if (tail != null) {
tail?.after = node
node.before = tail
tail = node
} else {
head = node
tail = node
}
manualLinkedList[key] = node
} finally {
lock.unlock()
}
}
// TTI过期策略
fun putWithTTI(key: K, value: V, tti: Duration) {
lock.lock()
try {
val expiresAt = Instant.now().plus(tti)
map[key] = TimestampedValue(value, expiresAt)
accessOrderMap[key] = value
// 手动维护访问顺序链表
val node = Node(key, value)
if (manualLinkedList.size == capacity) {
val oldestKey = head?.key
if (oldestKey != null) {
manualLinkedList.remove(oldestKey)
accessOrderMap.remove(oldestKey)
}
}
// 将新节点添加到链表尾部
if (tail != null) {
tail?.after = node
node.before = tail
tail = node
} else {
head = node
tail = node
}
manualLinkedList[key] = node
} finally {
lock.unlock()
}
}
fun get(key: K): V? {
lock.lock()
try {
val entry = map[key]
return if (entry != null && entry.expiresAt > Instant.now()) {
// 更新访问顺序
accessOrderMap[key] = entry.value
// 手动维护访问顺序链表
val node = manualLinkedList[key]
if (node != null) {
// 移除节点
if (node == head) {
head = node.after
}
if (node == tail) {
tail = node.before
}
node.before?.after = node.after
node.after?.before = node.before
// 将节点添加到链表尾部
tail?.after = node
node.before = tail
tail = node
if (head == null) {
head = node
}
}
entry.value
} else {
null
}
} finally {
lock.unlock()
}
}
fun remove(key: K): V? {
lock.lock()
try {
val value = map.remove(key)?.value
accessOrderMap.remove(key)
// 手动维护访问顺序链表
val node = manualLinkedList.remove(key)
if (node != null) {
if (node == head) {
head = node.after
}
if (node == tail) {
tail = node.before
}
node.before?.after = node.after
node.after?.before = node.before
}
return value
} finally {
lock.unlock()
}
}
fun clear() {
lock.lock()
try {
map.clear()
accessOrderMap.clear()
manualLinkedList.clear()
head = null
tail = null
} finally {
lock.unlock()
}
}
// 自动清理过期项
fun autoEvictExpiredItems(interval: Duration) {
val scheduler = Executors.newScheduledThreadPool(1)
scheduler.scheduleAtFixedRate({
lock.lock()
try {
map.values.removeIf {
it.expiresAt <= Instant.now() }
manualLinkedList.values.removeAll {
map[it.key]?.expiresAt <= Instant.now() }
} finally {
lock.unlock()
}
}, 0, interval.toMillis(), TimeUnit.MILLISECONDS)
}
// 获取当前缓存大小
fun size(): Int {
lock.lock()
try {
return map.size
} finally {
lock.unlock()
}
}
// 数据结构定义
private data class TimestampedValue<V>(val value: V, val expiresAt: Instant)
}
代码解释与优化
类名与变量名修正
将完整泛型缓存
改为符合Kotlin命名规范的CompleteGenericCache
。
中文变量名(如访问顺序链表
)改为英文命名(如accessOrderMap
),确保代码可编译。
构造函数补全
在构造函数中,使用传入的maxSize
初始化accessOrderMap
的容量,确保默认容量与用户指定的一致。
修正构造函数的语法错误,如括号闭合和初始化逻辑。
clear()
方法补全
在clear()
方法中,清空map
、accessOrderMap
、manualLinkedList
,并重置链表的头尾节点。
autoEvictExpiredItems
方法添加
添加定时任务自动清理过期项,使用scheduleAtFixedRate
定期执行清理操作,确保缓存中不会积累大量无效数据。
size()
方法添加
提供获取当前缓存大小的方法,便于监控缓存状态。
TimestampedValue
类定义
补充TimestampedValue
数据类的定义,用于存储值及其过期时间。
线程安全与锁机制
所有对共享资源的操作(如map
、accessOrderMap
、manualLinkedList
)均在ReentrantLock
的保护下执行,确保线程安全。
在get
、put
、remove
等方法中,使用lock.lock()
和lock.unlock()
包裹关键代码段,避免竞态条件。
LRU链表维护逻辑
在每次get
或put
操作后,更新链表指针,确保最近使用的节点位于链表尾部,最久未使用的节点位于头部。
当缓存达到容量限制时,移除头部节点(最旧的项),并更新链表结构。
异常处理与资源释放
在所有锁操作中使用try-finally
块,确保锁的正确释放,避免死锁。
在autoEvictExpiredItems
中,使用线程池执行定时任务,避免阻塞主线程。
使用示例
fun main() {
val cache = CompleteGenericCache<String, String>(capacity = 100)
// 存入带TTL的缓存项
cache.putWithTTL("key1", "value1", Duration.ofSeconds(10))
cache.putWithTTL("key2", "value2", Duration.ofSeconds(5))
// 获取缓存项
println(cache.get("key1")) // 输出 "value1"
println(cache.get("key2")) // 输出 "value2"
// 等待5秒后,key2过期
Thread.sleep(6000)
println(cache.get("key2")) // 输出 null
// 自动清理过期项
cache.autoEvictExpiredItems(Duration.ofSeconds(10))
Thread.sleep(20000) // 等待两次清理周期
println(cache.size()) // 输出 0
// 清空缓存
cache.clear()
println(cache.size()) // 输出 0
}
总结
通过上述实现,我们构建了一个功能全面的泛型缓存类,具备以下特点:
线程安全:使用ReentrantLock
确保多线程环境下的安全操作。
过期策略:支持TTL和TTI两种过期策略,自动清理无效数据。
容量控制:结合LRU算法和固定容量,防止内存溢出。
高效访问:使用ConcurrentHashMap
和ConcurrentLinkedHashMap
优化查找和插入性能。
灵活性:允许自定义容量、过期时间间隔,适应不同应用场景。
该实现适用于需要高性能缓存管理的Android应用或后端服务,开发者可根据需求进一步扩展功能,如添加统计信息、支持异步加载等。
暂无评论内容