404 Not Found

404 Not Found


nginx

Kotlin Generics Explained

Kotlin generics are safer than Java's — declaration-site variance lets Charlie decide covariance at definition time instead of repeating wildcards at every use site.

1. What You'll Learn


2. A Real Architect's Story

(1) Pain Point: Java Generic Wildcard Hell

Charlie writes List<? extends Order> for read-only order lists and List<? super Order> for write-only lists in Java. Every use site requires repeating wildcards, making code nearly unreadable.

(2) Kotlin Declaration-Site Variance Solution

JAVA
// Java: repeat wildcard at every use site
void process(List<? extends Order> orders) { }
void addAll(List<? super Order> target) { }

// Kotlin: declare variance once at definition site
class Repository<out T : Order> {  // Covariant at definition
    fun getAll(): List<T>  // T only appears in 'out' position
}
// No wildcards needed at use site!

Declaration-site variance: define once, use infinitely. Java use-site variance: repeat at every call site.


3. Generics Basics

(1) Generic Classes and Functions

KOTLIN
// Generic class
class Box<T>(val value: T) {
    fun unwrap(): T = value
}

val intBox = Box(42)         // T inferred as Int
val strBox = Box("Hello")   // T inferred as String

// Generic function
fun <T> singletonList(item: T): List<T> = listOf(item)

// Generic with type constraint
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b

// Multiple constraints
fun <T> process(item: T) where T : CharSequence, T : Comparable<T> {
    // T is both CharSequence and Comparable
}

(2) The Variance Problem: Why We Need out/in

KOTLIN
// This seems reasonable but is a TYPE ERROR
val strings: List<String> = listOf("Hello", "World")
// val objects: List<Any> = strings  // ERROR in Kotlin (OK in Java arrays)

// Why? If allowed:
// objects.add(42)  // Would corrupt the String list!
// strings[0].length  // ClassCastException at runtime

4. Declaration-Site Variance

(1) out (Covariance): Producer Only, Never Consumer

KOTLIN
// Producer: T only appears in 'out' position (return type)
interface Repository<out T> {
    fun findById(id: String): T
    fun findAll(): List<T>
    // fun save(item: T)  // ERROR: T in 'in' position
}

// Now this works:
val orderRepo: Repository<Order> = OrderRepositoryImpl()
val anyRepo: Repository<Any> = orderRepo  // OK! Covariant

(2) in (Contravariance): Consumer Only, Never Producer

KOTLIN
// Consumer: T only appears in 'in' position (parameter type)
interface Filter<in T> {
    fun test(item: T): Boolean
    fun filter(items: List<T>): List<T>
    // fun getResult(): T  // ERROR: T in 'out' position
}

// Now this works:
val orderFilter: Filter<Order> = object : Filter<Order> {
    override fun test(item: Order) = item.total > 1_000
    override fun filter(items: List<Order>) = items.filter { test(it) }
}
val anyFilter: Filter<Any> = orderFilter  // Cannot assign - contravariant direction

(3) Variance Rules at a Glance

Modifier Meaning T Appears In Subtype Direction Analog
out Covariant Only in return position Producer<Sub> is subtype of Producer<Base> Java ? extends
in Contravariant Only in parameter position Consumer<Base> is subtype of Consumer<Sub> Java ? super
None Invariant Any position No subtype relationship Java T

(4) Variance Flow Diagram

100%
flowchart TD
    subgraph "Covariant (out)"
        A1[Producer of Sub] -->|"IS-A"| A2[Producer of Base]
    end
    subgraph "Contravariant (in)"
        B1[Consumer of Base] -->|"IS-A"| B2[Consumer of Sub]
    end
    subgraph "Invariant"
        C1[Box of Sub] -.->|"NOT IS-A"| C2[Box of Base]
    end

5. Use-Site Variance (Type Projection)

When you can't decide variance at the declaration site, project it at the use site.

KOTLIN
// Use-site projection: when declaration-site is invariant
fun copyFrom(source: Array<out Any>, dest: Array<in Any>) {
    for (i in source.indices) {
        dest[i] = source[i]
    }
}

// Equivalent to Java:
// void copyFrom(Object[] source, Object[] dest)

(1) Use-Site Variance Comparison

Scenario Java Kotlin
Read-only parameter <? extends T> <out T>
Write-only parameter <? super T> <in T>
Declaration covariant Not supported out T
Declaration contravariant Not supported in T

6. Star Projection

When the generic type is unknown, use star projection for safe access.

KOTLIN
// List<*> = list of unknown type (safe to read, not write)
val unknownList: List<*> = listOf("Hello", 42, 3.14)
val first: Any? = unknownList[0]  // OK - returns Any?
// unknownList.add("New")  // ERROR - can't write

// Comparison
fun printSize(list: List<*>) {
    println("Size: ${list.size}")  // OK - size doesn't depend on T
}

// Array<*> is different
val array: Array<*> = arrayOf("A", "B")
// array[0] = "C"  // ERROR - can't write to Array<*>
val elem: Any? = array[0]  // OK - can read

(1) Star Projection Rules

Type Star Projection Equivalent Readable Writable
Foo<out T> Foo<out Any?> ✅ (Any?)
Foo<in T> Foo<in Nothing> ✅ (Nothing = unwritable)
Foo<T> (invariant, T upper bound) Foo<out Any?> ✅ (Any?)
Foo<T> (T upper bound T : Upper) Foo<out Upper> ✅ (Upper)

7. reified Type Parameters

(1) Type Erasure and reified

KOTLIN
// Normal generic: type is erased at runtime
fun <T> isA(value: Any): Boolean = value is T  // ERROR: Cannot check for instance of erased type

// reified: inline function preserves type at call site
inline fun <reified T> isA(value: Any): Boolean = value is T  // OK!

// Usage
isA<String>("Hello")  // true
isA<Int>(42)          // true
isA<Int>("42")        // false

// Practical: filter by type
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    return filter { it is T }.map { it as T }
}

(2) Practical reified Use Cases

KOTLIN
// JSON deserialization without Class parameter
inline fun <reified T> Json.decodeFromString(json: String): T

// Instead of:
// json.decodeFromString(Order::class.java, jsonString)

// Just:
// json.decodeFromString<Order>(jsonString)

8. Complete Example: Type-Safe Order Repository

KOTLIN
// ============================================
// OrderProcessor - Type-Safe Repository with Generics
// Feature: Generic repository with variance
// ============================================

open class Order(val id: String, val total: Double)
class BulkOrder(id: String, total: Double, val minQty: Int) : Order(id, total)
class PriorityOrder(id: String, total: Double, val priority: String) : Order(id, total)

// Covariant repository: only produces T
interface ReadOnlyRepository<out T : Order> {
    fun findById(id: String): T?
    fun findAll(): List<T>
}

// Contravariant writer: only consumes T
interface WriteRepository<in T : Order> {
    fun save(item: T)
    fun saveAll(items: List<T>)
}

// Full repository: invariant (both reads and writes)
interface OrderRepository<T : Order> : ReadOnlyRepository<T>, WriteRepository<T>

// In-memory implementation
class InMemoryOrderRepository<T : Order> : OrderRepository<T> {
    private val storage = mutableMapOf<String, T>()

    override fun findById(id: String): T? = storage[id]
    override fun findAll(): List<T> = storage.values.toList()
    override fun save(item: T) { storage[item.id] = item }
    override fun saveAll(items: List<T>) { items.forEach { save(it) } }
}

// Generic filter with contravariance
class OrderFilter<in T : Order>(private val predicate: (T) -> Boolean) {
    fun test(item: T): Boolean = predicate(item)
    fun filter(items: List<T>): List<T> = items.filter(predicate)
}

inline fun <reified T : Order> List<Order>.filterByType(): List<T> {
    return this.filterIsInstance<T>()
}

fun main() {
    val orders = listOf(
        Order("ORD-001", 299.99),
        BulkOrder("ORD-002", 5_000.00, 100),
        PriorityOrder("ORD-003", 15_000.00, "VIP"),
        BulkOrder("ORD-004", 8_000.00, 50),
        PriorityOrder("ORD-005", 2_500.00, "GOLD")
    )

    // Covariant: BulkOrder repo assignable to Order repo
    val bulkRepo: ReadOnlyRepository<BulkOrder> = InMemoryOrderRepository()
    val orderRepo: ReadOnlyRepository<Order> = bulkRepo  // OK: covariant

    // reified: filter by runtime type
    val bulkOrders: List<BulkOrder> = orders.filterByType<BulkOrder>()
    println("Bulk orders: ${bulkOrders.map { it.id }}")

    val priorityOrders: List<PriorityOrder> = orders.filterByType<PriorityOrder>()
    println("Priority orders: ${priorityOrders.map { it.id }}")

    // Generic filter
    val highValueFilter = OrderFilter<Order> { it.total > 1_000 }
    val highValue = highValueFilter.filter(orders)
    println("High value orders: ${highValue.map { it.id }}")
}

Output:

TEXT
Bulk orders: [ORD-002, ORD-004]
Priority orders: [ORD-003, ORD-005]
High value orders: [ORD-002, ORD-003, ORD-004, ORD-005]

❓ FAQ

Q Can out and in be used on the same type parameter simultaneously?
A No. A type parameter can only be out, in, or invariant. If T appears in both input and output positions, it must be invariant.
Q Do Kotlin generics still exist at runtime?
A Mostly erased (same as Java). But inline + reified can preserve type information. Inline functions are expanded at the call site, so type info is available at compile time.
Q What's the difference between star projection * and Any?
A List<*> means "element type unknown", readable only as Any?; List<Any> means "element type is Any", you can add any value. The former is safer.
Q When to use declaration-site variance vs use-site?
A Prefer declaration-site variance — decide at API design time whether T is read-only (out) or write-only (in). Use-site variance is a last resort.
Q Does reified have performance overhead?
A No additional runtime overhead. Since inline functions are expanded at compile time, reified type checks happen at compile time with no extra runtime work.
Q What's the difference between <T : Upper> and where T : Upper?
A Single constraint uses <T : Upper> shorthand; multiple constraints must use the where syntax.

📖 Summary


📝 Exercises

  1. Beginner (⭐): Implement a generic Box<T> and test assignment compatibility with different type arguments. Hint: can Box<Int> be assigned to Box<Any>?
  2. Intermediate (⭐⭐): Define ReadOnlyRepository<out T> and WriteRepository<in T>, demonstrate covariance and contravariance assignment relationships. Hint: List<BulkOrder>List<out Order>
  3. Advanced (⭐⭐⭐): Implement a spotNull<reified Any> function that filters by type and then casts. Hint: use filterIsInstance<T>

← Previous | Next →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏