Generics
Generic Classes and Functions
Section titled “Generic Classes and Functions”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 errorGeneric 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 bVariance
Section titled “Variance”Variance describes how subtyping relationships between generic type arguments compose with the Container type. Kotlin uses declaration-site variance with in and out modifiers.
Invariance
Section titled “Invariance”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 errorCovariance (out)
Section titled “Covariance (out)”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 // OKThe 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}Contravariance (in)
Section titled “Contravariance (in)”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 // OKThe compiler enforces that T only appears in in-positions (parameter types). It cannot appear As a return type.
Variance Mnemonic
Section titled “Variance Mnemonic”Producer<out T> -- produces T, covariantConsumer<in T> -- consumes T, contravariantDeclaration-Site vs Use-Site Variance
Section titled “Declaration-Site vs Use-Site Variance”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).
Type Constraints
Section titled “Type Constraints”Upper bounds restrict the types accepted as type arguments.
Single Bound
Section titled “Single Bound”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 ComparableMultiple Bounds
Section titled “Multiple Bounds”fun <T> ensureTrailingPeriod(seq: T): String where T : CharSequence, T : Appendable{ if (!seq.endsWith(".')) { seq.append('.') } return seq.toString()}Nullable Upper Bounds
Section titled “Nullable Upper Bounds”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
Section titled “Star Projection”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:
| Declaration | Star 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}Reified Types
Section titled “Reified Types”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")) // trueprintln(isType<Int>("hello")) // falseThe 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.
Common Reified Use Cases
Section titled “Common Reified Use Cases”// Type-safe castinginline fun <reified T> Any.castOrNull(): T? = this as? T
// Generic logginginline 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)}Reified Type Parameters in Inline Classes
Section titled “Reified Type Parameters in Inline Classes”inline class Id<T : Any>(val value: Long) { inline fun <reified R> isSameType(other: Id<R>): Boolean = T::class == R::class}Generics and Arrays
Section titled “Generics and Arrays”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-onlyGeneric Extension Properties
Section titled “Generic Extension Properties”val <T> List<T>.secondHalf: List<T> get() = subList(size / 2, size)
val <K, V> Map<K, V>.keysAsStrings: String get() = keys.joinToString(", ")Type Erasure and Runtime
Section titled “Type Erasure and Runtime”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 ArrayListReified types and inline functions are the primary mechanism for accessing generic type information At runtime in Kotlin.
Common Pitfalls
Section titled “Common Pitfalls”- ** Confusing
outandinvariance. Remember: producers areoutConsumers arein. If a class both produces and consumesTIt 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.
Intuition
Section titled “Intuition”Generics let you write code that works with any type while maintaining type safety. Think of a generic class as a mold that can produce objects of different types depending on what you pour in. Variance controls how subtypes relate: covariant types can only produce values (like a source), contravariant types can only consume values (like a sink), and invariant types do both. Type erasure means generic information disappears at runtime, which is why reified types use inline functions to preserve type information at compile time.
Summary
Section titled “Summary”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
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Collections — List
, Set , and Map<K,V> use generic type parameters; variance annotations control read/write access. - Coroutines — Deferred
and Flow are generic types that use variance to define producer/consumer relationships. - Coroutines Advanced — StateFlow
and SharedFlow are generic flows whose type parameters constrain what values can be emitted and collected. - Delegation and Result — Generic delegate classes and the Result
type use type parameters to provide type-safe delegation and error handling.