Generics trong Kotlin
🎯 Mục tiêu: Hiểu Generics - kỹ thuật viết code type-safe và reusable với type parameters, bao gồm variance (in/out) và type constraints.
💡 Vấn đề mà Generics giải quyết
Hãy tưởng tượng bạn cần viết một class Box để chứa đồ vật. Nếu không có Generics:
// ❌ Cách 1: Tạo nhiều class
class IntBox(val value: Int)
class StringBox(val value: String)
class UserBox(val value: User)
// Phải tạo class mới cho mỗi type!
// ❌ Cách 2: Dùng Any (mất type safety)
class AnyBox(val value: Any)
fun main() {
val box = AnyBox("Hello")
val str: String = box.value // ❌ Lỗi! Phải cast
val str2 = box.value as String // 😱 Có thể crash nếu không phải String
}Generics giải quyết cả hai vấn đề:
// ✅ Một class, mọi types, type-safe!
class Box<T>(val value: T)
fun main() {
val intBox = Box(42) // Box<Int>
val strBox = Box("Hello") // Box<String>
val userBox = Box(User("A")) // Box<User>
val number: Int = intBox.value // ✅ Type-safe, không cần cast!
val text: String = strBox.value // ✅ Compiler biết đây là String
}T là Type Parameter - placeholder cho một type cụ thể. Conventions:
T= Type (phổ biến nhất)E= Element (trong collections)K= Key,V= Value (trong Map)R= Return type
📝 Generic Class
Class cơ bản
// Generic class với một type parameter
class Box<T>(val value: T) {
fun getValue(): T = value
fun isNull(): Boolean = value == null
}
// Sử dụng
val intBox = Box(42)
val strBox = Box<String>("Hello") // Explicit type
val nullBox = Box<String?>(null) // Nullable typeClass với nhiều type parameters
class Pair<A, B>(val first: A, val second: B) {
override fun toString() = "($first, $second)"
}
class Triple<A, B, C>(val first: A, val second: B, val third: C)
fun main() {
val pair = Pair("Alice", 25) // Pair<String, Int>
val userScore = Pair(User("Bob"), 100.0) // Pair<User, Double>
println(pair.first) // Alice
println(pair.second) // 25
}Ví dụ thực tế: Generic Stack
class Stack<T> {
private val items = mutableListOf<T>()
fun push(item: T) {
items.add(item)
}
fun pop(): T? = items.removeLastOrNull()
fun peek(): T? = items.lastOrNull()
val isEmpty: Boolean
get() = items.isEmpty()
val size: Int
get() = items.size
override fun toString() = items.toString()
}
fun main() {
val stringStack = Stack<String>()
stringStack.push("First")
stringStack.push("Second")
stringStack.push("Third")
println(stringStack.pop()) // Third
println(stringStack.peek()) // Second
val numberStack = Stack<Int>()
numberStack.push(1)
numberStack.push(2)
println(numberStack) // [1, 2]
}🔧 Generic Function
Hàm cũng có thể có type parameters:
// Generic function
fun <T> singletonList(item: T): List<T> = listOf(item)
// Generic extension function
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
// Multiple type parameters
fun <T, R> List<T>.mapCustom(transform: (T) -> R): List<R> {
val result = mutableListOf<R>()
for (item in this) {
result.add(transform(item))
}
return result
}
fun main() {
val list = singletonList(42) // List<Int>
val strList = singletonList("Hello") // List<String>
val numbers = listOf(1, 2, 3)
println(numbers.secondOrNull()) // 2
val doubled = numbers.mapCustom { it * 2 }
println(doubled) // [2, 4, 6]
}Ví dụ thực tế: Swap function
fun <T> swap(array: Array<T>, i: Int, j: Int) {
val temp = array[i]
array[i] = array[j]
array[j] = temp
}
fun main() {
val numbers = arrayOf(1, 2, 3, 4, 5)
swap(numbers, 0, 4)
println(numbers.toList()) // [5, 2, 3, 4, 1]
val names = arrayOf("Alice", "Bob", "Charlie")
swap(names, 0, 2)
println(names.toList()) // [Charlie, Bob, Alice]
}🎯 Type Constraints
Upper Bound (Giới hạn trên)
Giới hạn type parameter phải là subtype của một type cụ thể:
// T phải là subtype của Comparable<T>
fun <T : Comparable<T>> max(a: T, b: T): T {
return if (a > b) a else b
}
fun main() {
println(max(5, 10)) // 10
println(max("apple", "banana")) // banana
println(max(3.14, 2.71)) // 3.14
// ❌ Lỗi compile: User không implement Comparable
// println(max(User("A"), User("B")))
}Multiple Constraints với where
Khi cần nhiều constraints:
interface Drawable {
fun draw()
}
interface Clickable {
fun onClick()
}
// T phải implement cả Drawable và Clickable
fun <T> renderAndHandle(widget: T) where T : Drawable, T : Clickable {
widget.draw()
widget.onClick()
}
// Ví dụ thực tế
class Button : Drawable, Clickable {
override fun draw() = println("Drawing button")
override fun onClick() = println("Button clicked!")
}
fun main() {
renderAndHandle(Button())
// Drawing button
// Button clicked!
}Nullable constraints
// Mặc định T có thể null (upper bound là Any?)
fun <T> process(value: T) {
println(value) // value có thể null
}
// T không thể null (upper bound là Any)
fun <T : Any> processNonNull(value: T) {
println(value.toString()) // An toàn, value không null
}
fun main() {
process(null) // ✅ OK
// processNonNull(null) // ❌ Lỗi compile
}📊 Variance: in và out
Vấn đề covariance
open class Animal
class Dog : Animal()
class Cat : Animal()
fun main() {
val dogs: List<Dog> = listOf(Dog(), Dog())
// ✅ OK vì List là covariant (out)
val animals: List<Animal> = dogs
// Nhưng với MutableList:
val mutableDogs: MutableList<Dog> = mutableListOf(Dog())
// ❌ Lỗi compile! MutableList không phải covariant
// val mutableAnimals: MutableList<Animal> = mutableDogs
}out = Producer (Covariant)
out nghĩa là type chỉ được xuất ra (produce), không được nhận vào (consume):
// Producer chỉ OUTPUT type T
interface Producer<out T> {
fun produce(): T
// fun consume(item: T) // ❌ Không được! out = không nhận input
}
class DogProducer : Producer<Dog> {
override fun produce() = Dog()
}
fun main() {
val dogProducer: Producer<Dog> = DogProducer()
// ✅ OK vì out - Dog là subtype của Animal
val animalProducer: Producer<Animal> = dogProducer
val animal: Animal = animalProducer.produce() // Nhận được Dog
}in = Consumer (Contravariant)
in nghĩa là type chỉ được nhận vào (consume), không được xuất ra (produce):
// Consumer chỉ INPUT type T
interface Consumer<in T> {
fun consume(item: T)
// fun produce(): T // ❌ Không được! in = không output
}
class AnimalHandler : Consumer<Animal> {
override fun consume(item: Animal) {
println("Handling animal")
}
}
fun main() {
val animalHandler: Consumer<Animal> = AnimalHandler()
// ✅ OK vì in - Animal handler có thể handle Dog
val dogHandler: Consumer<Dog> = animalHandler
dogHandler.consume(Dog())
}Tóm tắt Variance
| Keyword | Tên | Hướng | Ví dụ real-world |
|---|---|---|---|
out T | Covariant | Chỉ output (produce) | List<out T>, Flow<out T> |
in T | Contravariant | Chỉ input (consume) | Comparable<in T> |
| (không có) | Invariant | Cả hai | MutableList<T> |
Nhớ đơn giản:
- OUT = Output only = Producer = “Tôi cho ra T”
- IN = Input only = Consumer = “Tôi nhận vào T”
⭐ Star Projection
Khi không quan tâm đến type cụ thể:
fun printListSize(list: List<*>) {
println("List size: ${list.size}")
// list[0] có type Any? vì chúng ta không biết type cụ thể
}
fun main() {
printListSize(listOf(1, 2, 3)) // List size: 3
printListSize(listOf("a", "b")) // List size: 2
}🛠️ Thực hành
Bài tập 1: Generic Result class
// TODO: Tạo sealed class Result<T> với:
// - Success(data: T)
// - Error(message: String)
// - LoadingLời giải:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
data object Loading : Result<Nothing>()
fun getOrNull(): T? = when (this) {
is Success -> data
else -> null
}
fun getOrElse(default: @UnsafeVariance T): T = when (this) {
is Success -> data
else -> default
}
}
fun main() {
val success: Result<Int> = Result.Success(42)
val error: Result<Int> = Result.Error("Something went wrong")
println(success.getOrNull()) // 42
println(error.getOrNull()) // null
println(error.getOrElse(0)) // 0
}Bài tập 2: Generic Cache class
// TODO: Tạo class Cache<K, V> với:
// - put(key: K, value: V)
// - get(key: K): V?
// - contains(key: K): Boolean
// - remove(key: K): V?
// - clear()Lời giải:
class Cache<K, V> {
private val storage = mutableMapOf<K, V>()
fun put(key: K, value: V) {
storage[key] = value
}
fun get(key: K): V? = storage[key]
fun contains(key: K): Boolean = storage.containsKey(key)
fun remove(key: K): V? = storage.remove(key)
fun clear() = storage.clear()
val size: Int get() = storage.size
}
fun main() {
val userCache = Cache<Int, String>()
userCache.put(1, "Alice")
userCache.put(2, "Bob")
println(userCache.get(1)) // Alice
println(userCache.contains(3)) // false
userCache.remove(1)
println(userCache.size) // 1
}📱 Trong Android
// Generic Response wrapper
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error(val code: Int, val message: String) : ApiResponse<Nothing>()
data object Loading : ApiResponse<Nothing>()
}
// Repository với generic function
class UserRepository {
suspend fun <T> safeApiCall(apiCall: suspend () -> T): ApiResponse<T> {
return try {
ApiResponse.Success(apiCall())
} catch (e: Exception) {
ApiResponse.Error(500, e.message ?: "Unknown error")
}
}
}
// ViewModel với generic LiveData
class BaseViewModel<T> : ViewModel() {
protected val _data = MutableLiveData<ApiResponse<T>>()
val data: LiveData<ApiResponse<T>> = _data
}
// Adapter với generic ViewHolder
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()
}
}⚠️ Lưu ý quan trọng
Type Erasure: Thông tin generic bị xóa lúc runtime!
fun main() {
val intList = listOf(1, 2, 3)
val strList = listOf("a", "b")
// ❌ Không thể check generic type lúc runtime
// if (intList is List<Int>) { } // Warning: Unchecked cast
// ✅ Dùng reified với inline function
}Reified type parameters: Giữ thông tin type lúc runtime
inline fun <reified T> isType(value: Any): Boolean {
return value is T
}
fun main() {
println(isType<String>("Hello")) // true
println(isType<Int>("Hello")) // false
}✅ Checklist - Tự kiểm tra
Sau bài học này, bạn có thể:
- Hiểu vấn đề mà Generics giải quyết (type safety + reusability)
- Tạo generic class với một hoặc nhiều type parameters
- Tạo generic function và extension function
- Sử dụng type constraints với
: Typevàwhere - Hiểu variance:
out(covariant) vàin(contravariant) - Biết khi nào dùng star projection
* - Hiểu type erasure và giải pháp với
reified
Tiếp theo: Type Aliases