Skip to content

Generics | Kotlin - Wyatt's Notes

Generics allow types to be parameterized. The compiler enforces type safety at compile time, and the JVM erases generic type parameters at runtime (type erasure).

class Box<T>(val value: T)
val intBox: Box<Int> = Box(42)
val strBox: Box<String> = Box("hello")
// val wrong: Box<Int> = Box("hello") // compile error

Generic functions:

fun <T> singletonList(item: T): List<T> = listOf(item)
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b

Variance describes how subtyping relationships between generic type arguments compose with the Container type. Kotlin uses declaration-site variance with in and out modifiers.

By default, generic types are invariant. Box<Dog> is not a subtype of Box<Animal>.

class Container<T>
val dogs: Container<Dog> = Container()
// val animals: Container<Animal> = dogs // compile error

If a generic type is only used as output (produced), it can be declared covariant with out. Producer<Dog> is a subtype of Producer<Animal>.

class Producer<out T>(private val value: T) {
fun get(): T = value
}
val dogProducer: Producer<Dog> = Producer(Dog())
val animalProducer: Producer<Animal> = dogProducer // OK

The compiler enforces that T only appears in out-positions (return types). It cannot appear as A parameter type or in a mutable property.

class Producer<out T>(private val value: T) {
fun get(): T = value // OK -- T in return position
// fun set(t: T) {} // compile error -- T in parameter position
}

If a generic type is only used as input (consumed), it can be declared contravariant with in. Consumer<Animal> is a subtype of Consumer<Dog>.

class Consumer<in T> {
fun consume(value: T) {}
}
val animalConsumer: Consumer<Animal> = Consumer()
val dogConsumer: Consumer<Dog> = animalConsumer // OK

The compiler enforces that T only appears in in-positions (parameter types). It cannot appear As a return type.

Producer<out T> -- produces T, covariant
Consumer<in T> -- consumes T, contravariant

Kotlin prefers declaration-site variance (Java only has use-site). Declaration-site variance means The class author decides the variance once, and all call sites benefit.

// Declaration-site (Kotlin style)
class Box<out T>(val value: T)
// Use-site (Java style, also available in Kotlin)
fun copy(from: Array<out Any>, to: Array<in Any>) {
for (i in from.indices) {
to[i] = from[i]
}
}

Use-site variance is useful when declaration-site variance is not possible (e.g., arrays, which are Invariant on the JVM).

Upper bounds restrict the types accepted as type arguments.

fun <T : Comparable<T>> sort(list: MutableList<T>) {
list.sort()
}
sort(mutableListOf(3, 1, 2)) // OK -- Int : Comparable<Int>
// sort(mutableListOf(any())) // compile error -- Any is not Comparable
fun <T> ensureTrailingPeriod(seq: T): String
where T : CharSequence,
T : Appendable
{
if (!seq.endsWith(".')) {
seq.append('.')
}
return seq.toString()
}
fun <T : Any> requireNonNull(value: T?): T {
return value ?: throw IllegalArgumentException("Value is null")
}

T : Any constrains T to non-nullable types. Without this bound, T defaults to T : Any? Allowing nullable types.

Star projection (*) is used when the exact type argument is unknown or irrelevant. It is the Kotlin equivalent of Java’s raw types but is type-safe.

fun printAll(list: List<*>) {
for (item in list) {
println(item) // item has type Any?
}
}

Star projection behavior depends on variance:

DeclarationStar projection * means
Producer<out T>Producer<out Nothing>
Consumer<in T>Consumer<in Any?>
MutableList<T>MutableList<out Nothing> (read-only)
fun process(producer: Producer<*>) {
val value: Any? = producer.get() // OK
}
fun process(consumer: Consumer<*>) {
// consumer.consume(value) // compile error -- cannot write to star projection
}

Type parameters are erased at runtime, so you cannot perform type checks or create instances of a Type parameter directly.

// This does not compile:
// fun <T> isType(value: Any): Boolean = value is T
// Using reified:
inline fun <reified T> isType(value: Any): Boolean = value is T
println(isType<String>("hello")) // true
println(isType<Int>("hello")) // false

The reified modifier requires the function to be inline. The compiler substitutes the actual Type argument at each call site, making the type available at runtime.

// Type-safe casting
inline fun <reified T> Any.castOrNull(): T? = this as? T
// Generic logging
inline fun <reified T> T.log(): T {
println("${T::class.simpleName}: $this")
return this
}
// Intent extras (Android)
inline fun <reified T : Parcelable> Intent.getParcelable(key: String): T? {
return getParcelableExtra(key)
}
inline class Id<T : Any>(val value: Long) {
inline fun <reified R> isSameType(other: Id<R>): Boolean =
T::class == R::class
}

Arrays on the JVM are covariant and reified (type information is preserved at runtime). Kotlin’s Generics are invariant and erased. This mismatch requires attention.

// Array is covariant (JVM behavior)
val strings: Array<String> = arrayOf("a", "b")
val objects: Array<Any> = strings // compile error in Kotlin (unlike Java)
// Kotlin prevents this for safety. Use Array<out Any> if needed:
val objects: Array<out Any> = strings // OK -- read-only
val <T> List<T>.secondHalf: List<T>
get() = subList(size / 2, size)
val <K, V> Map<K, V>.keysAsStrings: String
get() = keys.joinToString(", ")

Generic type parameters are erased at runtime. Two instances of List<String> and List<Int> are Both List at runtime.

val stringList = listOf("a", "b")
val intList = listOf(1, 2)
println(stringList::class == intList::class) // true -- both are ArrayList

Reified types and inline functions are the primary mechanism for accessing generic type information At runtime in Kotlin.

Variance is about subtyping relationships: Imagine a box of animals. Can you put a Dog box where an Animal box is expected? Covariance (out) says yes — a Produceris a Producer. Contravariance (in) says the opposite — a Consumercan consume Dogs. Invariance says no conversion is safe.

Why it matters: Variance determines how generic types relate to each other in inheritance hierarchies. Getting it wrong causes compile errors or, worse, runtime ClassCastExceptions. Kotlin’s declaration-site variance (using in/out on the class) is cleaner than Java’s use-site variance.

The key insight: Reified type parameters work because inline functions are expanded at compile time — the actual type is substituted at each call site, making runtime type information available.

  • ** Confusing out and in variance. Remember: producers are outConsumers are in. If a class both produces and consumes TIt must be invariant.
  • ** Using star projection when you actually need the type. Star projection treats the type as unknown, which means you can read as Any? but cannot write. If you need the specific type, pass the type parameter explicitly.
  • ** Forgetting that reified type parameters require inline. Non-inline functions cannot have reified type parameters.
  • ** Storing reified type parameters in collections. Since reified works through inline expansion, you cannot store a type parameter for later use. The type information is only available at the call site during compilation.
  • *_ Using List<_> and then trying to add elements. Star projection makes the list effectively read-only for generic types.
flowchart TD
    A[Generics] --> B[Key Concepts]
    A --> C[Core Principles]
    A --> D[Practical Applications]
    B --> E[Fundamental definitions]
    C --> F[Design patterns]
    D --> G[Real-world usage]

This topic covers the core concepts of generics, including underlying theory, practical implementation, and key applications.

Key concepts include:

  • core concepts and terminology
  • algorithms and computational thinking
  • practical implementation
  • security and ethical considerations
  • applications in the real world

Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.

Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.

  • Site Home: Main landing page for kotlin notes.
  • Practice: Practice problems for revision.