Kotlin programming language
Kotlin programming language
Generics

Kotlin programming language

What? Why?

fun quickSort(collection: CollectionOfInts) { ... }
quickSort(listOf(1, 2, 3))          // OK
quickSort(listOf(1.0, 2.0, 3.0))    // NOT OK

fun quickSort(collection: CollectionOfDoubles) { ... } // overload
quickSort(listOf(1.0, 2.0, 3.0))    // OK 
quickSort(listOf(1, 2, 3))          // OK

Kotlin Number inheritors: Int, Double, Byte, Float, Long, Short
Do we need 4 more implementations of quickSort?

Kotlin programming language

How?

Does the quickSort algorithm actually care what is it sorting?
No, as long as it can compare two values against each other.

fun <T : Comparable<T>> quickSort(collection: Collection<T>): Collection<T> { ... }

quickSort(listOf(1.0, 2.0, 3.0))            // OK 

quickSort(listOf(1, 2, 3))                  // OK

quickSort(listOf("one", "two", "three"))    // OK
Kotlin programming language

Type Parameters

A type parameter is a placeholder for a type, resolved at the call site.

Generics let you write code that works with any type
or any type satisfying a constraint
without duplicating it

class Holder<T>(val value: T) { ... }

val intHolder = Holder<Int>(23)

val cupHolder = Holder("cup")   // Generic parameter type can be inferred
Kotlin programming language

Constraints

An upper bound constrains a type parameter to a specific type or its subtypes - ensuring the type provides the required functionality.

class Pilot<T : Movable>(val vehicle: T) { 
    fun go() { vehicle.move() }
}

val ryanGosling = Pilot<Car>(Car("Chevy", "Malibu"))
val sullySullenberger = Pilot<Plane>(Plane("Airbus", "A320"))
Kotlin programming language

Constraints #2

There can be several parameter types, and generic classes can participate in inheritance

public interface MutableMap<K, V> : Map<K, V> { ... }

There can also be several constraints (meaning the type parameter has to implement several interfaces):

fun <T, S> moveInAnAwesomeWayAndCompare(a: T, b: S) 
    where T: Comparable<T>, S: Comparable<T>, T: Awesome, T: Movable 
    { ... }

Note: S: Comparable<T> is intentional — it constrains S to be comparable to T (cross-type), not just comparable to itself. This allows b.compareTo(a) inside the function.

Kotlin programming language

Star-projection

When you do not care about the parameter type, you can use star-projection * (Any? / Nothing). For example, all maps have the same size:Int property, independent of their specific generic parameters

fun printSize(map: MutableMap<*, *>) { println(map.size) }

Note: Working with methods that actually use generic parameters is almost impossible when using star-projection. For example, MutableList<*> will have add(element: Nothing) and get(index: Int): Any?

Kotlin programming language

Back to Subtyping

open class A
open class B : A()
class C : B()

Nothing <: C <: B <: A <: Any

This means that the Any is the superclass for all the classes
At the same time Nothing is a subtype of any type

Note: <: is standard notation for "is a subtype of" in type theory, but in Kotlin we use :

Kotlin programming language

Variance problem

a.k.a. Does subtyping carry over?

interface Holder<T> {
    fun push(newValue: T)   // consumes an element

    fun pop(): T            // produces an element

    fun size(): Int         // does not interact with T
}

C <: B holds — does Holder<C> <: Holder<B> hold too?
What about the reverse: does Holder<B> <: Holder<C>?

How relationship between types A and B affects the relationship between Holder<A> and Holder<B>?

Kotlin programming language

Variance annotations

interface Holder<T> {
    fun push(newValue: T)   // consumes an element
    fun pop(): T            // produces an element
    fun size(): Int         // does not interact with T
}

Variance modifiers:

G<T> - invariant, can consume and produce elements
G<in T> - contravariant, can only consume elements
G<out T> - covariant, can only produce elements
G<*> - star-projection, does not interact with T

Splitting Holder<T> by role allows the compiler to enforce safe subtype relationships

Kotlin programming language

Variance example #1

G<T> // invariant, can consume and produce elements
interface Holder<T> {
    fun push(newValue: T) // consumes an element: OK

    fun pop(): T // produces an element: OK

    fun size(): Int // does not interact with T: OK
}
Kotlin programming language

Variance example #2

G<in T> // contravariant, can only consume elements
interface Holder<in T> {
    fun push(newValue: T) // consumes an element: OK

    fun pop(): T // produces an element: ERROR: [TYPE_VARIANCE_CONFLICT_ERROR] 
                 // Type parameter T is declared as 'in' but occurs in 'out' position in type T

    fun size(): Int // does not interact with T: OK
}
Kotlin programming language

Variance example #3

G<out T> // covariant, can only produce elements
interface Holder<out T> {
    fun push(newValue: T) // consumes an element: ERROR: [TYPE_VARIANCE_CONFLICT_ERROR] 
                        // Type parameter T is declared as 'out' but occurs in 'in' position in type T

    fun pop(): T // produces an element: OK

    fun size(): Int // does not interact with T: OK
}

Kotlin programming language

Variance example #4


interface Holder<T> {
    fun push(newValue: T) // consumes an element: OK
    fun pop(): T // produces an element: OK
    fun size(): Int // does not interact with T: OK
}

fun <T> foo1(holder: Holder<T>, t: T) {
    holder.push(t) // OK
}

fun <T> foo2(holder: Holder<*>, t: T) {
    holder.push(t) // ERROR: [TYPE_MISMATCH] Type mismatch. Required: Nothing. Found: T
}

Star-projection makes the input type Nothing - you can never safely call a consuming method on Holder<*>

Kotlin programming language

Subtyping and variance

open class A
open class B : A()      —--->  Nothing <: C <: B <: A <: Any
class C : B()
class Holder<T>(val value: T) { ... }
Holder<Nothing> ??? Holder<C> ??? Holder<B> ??? Holder<A> ??? Holder<Any>
Kotlin programming language

Subtyping and variance #2

open class A
open class B : A()      —--->  Nothing <: C <: B <: A <: Any
class C : B()
class Holder<T>(val value: T) { ... }

Generics are invariant by default

Holder<Nothing> ≁ Holder<C> ≁ Holder<B> ≁ Holder<A> ≁ Holder<Any>
val c: C = C()
val b: B = c // C <: B, OK
VS
val holderC: Holder<C> = Holder(C())
val holderB: Holder<B> = holderC 
// ERROR: Type mismatch. Required: Holder<B>. Found: Holder<C>
Kotlin programming language

Subtyping and variance #3

If the assignment were allowed, holderB.push(B()) would silently put a B into what is actually a Holder<C> — breaking type safety.

val holderC: Holder<C> = Holder(C())
val holderB: Holder<B> = holderC    // imagine this were allowed

holderB.push(B())                   // putting a B into a Holder<C> — type violation!
val c: C = holderB.pop()            // would also return a B as C — type violation!

C <: B holds — a C is a valid B, but not the other way around.

Holder<C> <: Holder<B> does not hold - Holder<T> can both consume and produce. Allowing either subtype direction would create a hole in the type system.

Kotlin programming language

Subtyping and variance #4

class C : B()

class Holder<T>(val value: T) { ... }

val holderC: Holder<C> = Holder(C())
val holderB: Holder<B> = holderC //ERROR: Type mismatch. Required: Holder<B>. Found: Holder<C>.

BUT

val holderB: Holder<B> = Holder(C())

Expected type on the left: Holder<B>

compiler infers T = B

the call is effectively Holder<B>(C())

Kotlin programming language

Subtyping and variance #5

class Holder<T> (var value: T?) {
   fun pop(): T? = value.also { value = null }
   fun push(newValue: T?): T? = value.also { value = newValue }
   fun steal(other: Holder<T>) { value = other.pop() }
   fun gift(other: Holder<T>) { other.push(pop()) }
}

Holder<Nothing> ≁ Holder<C> ≁ Holder<B> ≁ Holder<A> ≁ Holder<Any>

val holderB: Holder<B> = Holder(B())
val holderA: Holder<A> = Holder(null)
holderA.steal(holderB) // ERROR: Type mismatch. Required: Holder<A>. Found: Holder<B>.
holderB.gift(holderA) // ERROR: Type mismatch. Required: Holder<B>. Found: Holder<A>.

Invariance is enforced at every boundary — not just assignment, but also when passing generics as function arguments

Kotlin programming language

Type projection: in

class Holder<T> (var value: T?) {
    fun pop(): T? = value.also { value = null }
    fun push(newValue: T?): T? = value.also { value = newValue }
    fun gift(other: Holder<in T>) { other.push(pop()) }
}
holderB.gift(holderA) // OK

Type projection: other is a restricted (projected) generic. Only methods where T appears in input (parameter) position are usable — in this case only push().

This is contravariance — the subtype hierarchy flips (:> means "is a supertype of"):

Nothing <: C <: B <: A <: Any
Holder<in Nothing> :> Holder<in C> :> Holder<in B> :> Holder<in A> :> Holder<in Any>
Kotlin programming language

Type projection: out

class Holder<T> (var value: T?) {
    fun pop(): T? = value.also { value = null }
    fun push(newValue: T?): T? = value.also { value = newValue }
    fun steal(other: Holder<out T>) { value = other.pop() }
}
holderA.steal(holderB) // OK

Type projection: other is a restricted (projected) generic. Only methods where T appears in output (return) position are usable — in this case only pop().

This is covariance — the subtype hierarchy is preserved:

Nothing <: C <: B <: A <: Any
Holder<out Nothing> <: Holder<out C> <: Holder<out B> <: Holder<out A> <: Holder<out Any>
Kotlin programming language

Type projections

class Holder<T> (var value: T?) {
    fun steal(other: Holder<out T>) { 
        val oldValue = push(other.pop())
        other.push(oldValue) // ERROR: Type mismatch. Required: Nothing?. Found: T?.
    }
    fun gift(other: Holder<in T>) { 
        val otherValue = other.push(pop())
        push(otherValue) // ERROR: Type mismatch. Required: T?. Found: Any?.
    }
}

out T — readable as T?, not writable (write position becomes Nothing?)
in T — writable as T?, not readable (read position becomes Any?)

Kotlin programming language

Type erasure

At runtime, generic type arguments are erased — all instances share the same bytecode regardless of their type parameter.

Unbounded T erases to Any?; bounded T : Movable erases to Movable.

At the JVM level, MutableMap<String, Int> and MutableMap<Long, Boolean>
are the same raw MutableMap — type arguments are gone.

In Kotlin source, this erased state is expressed as MutableMap<*, *>

Pilot<Car> and Pilot<Plane> both become Pilot<Movable> at the JVM level (erased to the bound).

Kotlin programming language

Type erasure #2

As a corollary, you cannot override a function (in Kotlin/JVM) by changing generic type parameters:

fun quickSort(collection: Collection<Int>) { ... }
fun quickSort(collection: Collection<Double>) { ... }

Both become quickSort(collection: Collection<*>) and their signatures clash.

But you can use the JvmName annotation:

@JvmName("quickSortInt")
fun quickSort(collection: Collection<Int>) { ... }
fun quickSort(collection: Collection<Double>) { ... }
Kotlin programming language

Nullability in Generics

An unbounded type parameter T has an implicit upper bound of Any?, so nullable substitutions are allowed by default.

class Holder<T>(val value: T) { ... }       // Notice there is no `?`
val holderA: Holder<A?> = Holder(null)      // T = A? and that is OK

To prohibit nullable type arguments, constrain T with Any.

class Holder<T : Any>(val value: T) { ... }
val holderA: Holder<A?> = Holder(null)         // ERROR: Expected: Any. Found: A?

T & Any is Kotlin's definite non-null type — the intersection of T and Any, which strips null from T even when T is nullable.

fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y
Kotlin programming language

Thanx!