Step 5 of 13
Generic functions and classes, type constraints, variance (in/out), star projection, reified types
Generics ใน Kotlin — type constraints, variance (in/out), star projection, และ reified types
Generics let you write code that works with different types while keeping type safety:
// Without generics — type safety lost
val list: MutableList<Any> = mutableListOf("hello", 42)
val first = list[0] as String // risky cast
// With generics — type safety guaranteed
val names: MutableList<String> = mutableListOf("Alice", "Bob")
val first = names[0] // no cast needed, type is String
fun <T> printList(items: List<T>) {
for (item in items) {
println(item)
}
}
fun main() {
printList(listOf("Alice", "Bob")) // Alice \n Bob
printList(listOf(1, 2, 3)) // 1 \n 2 \n 3
}
class Box<T>(val content: T) {
fun describe(): String = "Box contains: $content"
}
fun main() {
val stringBox = Box("Hello")
val intBox = Box(42)
println(stringBox.describe()) // Box contains: Hello
println(intBox.describe()) // Box contains: 42
}
Restrict which types are allowed using where or inline constraints:
// Inline constraint: T must be Number
fun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
println(sum(3, 4)) // 7.0
println(sum(3.5, 2.5)) // 6.0
// sum("a", "b") // Compilation error: String is not Number
// Multiple constraints with where
class Calculator<T> where T : Number, T : Comparable<T> {
fun max(a: T, b: T): T {
return if (a >= b) a else b
}
}
Variance controls how generic types relate to each other. This matters when you assign a List<String> to a variable typed List<Any>.
out — Covariant (Producer)Use out when the type is only returned (produced), never consumed:
// List<out E> — you can only READ from it
val strings: List<String> = listOf("A", "B")
val objects: List<Any> = strings // OK because List is covariant (out)
println(objects) // [A, B]
Rule: out types are safe to read from. Think: producer uses out.
in — Contravariant (Consumer)Use in when the type is only consumed (passed in), never returned:
interface Comparator<in T> {
fun compare(a: T, b: T): Int
}
// A comparator that can compare Any objects
val anyComparator: Comparator<Any> = Comparator { a, b -> a.hashCode() - b.hashCode() }
// Can be used where a String comparator is needed
val stringComparator: Comparator<String> = anyComparator
Rule: in types are safe to write to. Think: consumer uses in.
| Modifier | Can read? | Can write? | Mnemonic |
|---|---|---|---|
out T | Yes (T) | No | Producer — out |
in T | No | Yes (T) | Consumer — in |
T (default) | Yes | Yes | Invariant — no substitution |
*Use * when you do not care about the type parameter:
fun printSize(list: List<*>) {
println("Size: ${list.size}")
// list[0] returns Any? — you don't know the type
}
printSize(listOf("A", "B")) // Size: 2
printSize(listOf(1, 2, 3)) // Size: 3
Normally, generic type parameters are erased at runtime (type erasure). With reified, you can access the type at runtime. This requires an inline function:
inline fun <reified T> getTypeName(): String {
return T::class.simpleName ?: "Unknown"
}
println(getTypeName<String>()) // String
println(getTypeName<Int>()) // Int
This is useful for filtering by type:
inline fun <reified T> List<*>.filterByType(): List<T> {
return this.filterIsInstance<T>()
}
val mixed: List<Any> = listOf("hello", 42, "world", 99)
val strings: List<String> = mixed.filterByType()
println(strings) // [hello, world]
Write a generic Repository that stores items by ID:
class Repository<T, ID>(val idSelector: (T) -> ID) {
private val items = mutableMapOf<ID, T>()
fun save(item: T) {
items[idSelector(item)] = item
}
fun findById(id: ID): T? = items[id]
fun findAll(): List<T> = items.values.toList()
}
data class User(val id: Int, val name: String)
fun main() {
val repo = Repository<User, Int> { it.id }
repo.save(User(1, "Alice"))
repo.save(User(2, "Bob"))
println(repo.findById(1)) // User(id=1, name=Alice)
println(repo.findAll()) // [User(id=1, name=Alice), User(id=2, name=Bob)]
}