DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Kotlin Generics Deep Dive: Variance, Reified & Type Constraints

Kotlin Generics Deep Dive: Variance, Reified & Type Constraints

Kotlin's generic system is more powerful than Java's. This guide covers variance, reified type parameters, and practical type constraints.

Understanding Variance

Kotlin has covariance (out) and contravariance (in):

// Covariance: Producer of T
interface Producer<out T> {
    fun produce(): T
}

// Contravariance: Consumer of T
interface Consumer<in T> {
    fun consume(item: T)
}

// Invariance: Both producer and consumer (default)
interface Storage<T> {
    fun store(item: T)
    fun retrieve(): T
}
Enter fullscreen mode Exit fullscreen mode

Covariance allows safer type substitution:

val stringProducer: Producer<String> = object : Producer<String> {
    override fun produce() = "Hello"
}
val anyProducer: Producer<Any> = stringProducer // Valid due to covariance
Enter fullscreen mode Exit fullscreen mode

Reified Type Parameters

Reified parameters preserve type information at runtime:

inline fun <reified T> parseJson(json: String): T {
    return Gson().fromJson(json, T::class.java)
}

data class User(val name: String, val age: Int)
val user = parseJson<User>(jsonString)
Enter fullscreen mode Exit fullscreen mode

Without reified, type information is erased:

// This would NOT work
fun <T> parseJsonBad(json: String): T {
    // Cannot access T::class here - type is erased
    return TODO()
}
Enter fullscreen mode Exit fullscreen mode

Type Constraints & Upper Bounds

Constrain type parameters with upper bounds:

// T must be Comparable
fun <T : Comparable<T>> findMax(items: List<T>): T? {
    return items.maxOrNull()
}

// Multiple bounds
fun <T> process(item: T) where T : Serializable, T : Comparable<T> {
    // T must implement both Serializable and Comparable
}
Enter fullscreen mode Exit fullscreen mode

Practical Pattern: Generic Extension

fun <T : Any> Any?.requireOfType(): T {
    require(this is T) {
        "Expected ${T::class.simpleName}, got ${this?.javaClass?.simpleName}"
    }
    return this as T
}

val obj: Any = "Hello"
val str: String = obj.requireOfType() // Safe cast with validation
Enter fullscreen mode Exit fullscreen mode

Type Erasure & Solutions

Despite some erasure, Kotlin provides workarounds:

// Check instance with erased type (works for reified only)
inline fun <reified T> isInstance(obj: Any) = obj is T

// Store type information explicitly
class TypeToken<T>
val listType = object : TypeToken<List<String>>() {}
Enter fullscreen mode Exit fullscreen mode

8 Android app templates on Gumroad

Top comments (0)