Generics | Kotlin - Wyatt's Notes
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.
Intuition
Section titled “Intuition”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 Producer
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.
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.
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]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.