Skip to Content

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 }
💡

TType 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 type

Class 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: inout

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

KeywordTênHướngVí dụ real-world
out TCovariantChỉ output (produce)List<out T>, Flow<out T>
in TContravariantChỉ input (consume)Comparable<in T>
(không có)InvariantCả haiMutableList<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) // - Loading

Lờ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 : Typewhere
  • 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

Last updated on