Generics, variance and delegation

Type parameters and bounds, declaration-site and use-site variance, star projections, reified inline generics, and delegation with by.

Variance explained by use

// out: a producer. Read values out of it, never put them in.
interface Source<out T> {
    fun next(): T
}

// in: a consumer. Put values in, never read them out.
interface Sink<in T> {
    fun accept(value: T)
}

class NumberSource : Source<Number> {
    override fun next(): Number = 42
}

// Source<Int> is a subtype of Source<Number> because T is covariant
val source: Source<Number> = NumberSource()
val widened: Source<Any> = source

// List is declared as List<out E>, so this is legal
fun printAll(items: List<Any>) = items.forEach(::println)
printAll(listOf(1, "two", 3.0))

// MutableList is invariant: writing and reading make both directions unsafe
// fun mutate(items: MutableList<Any>) { }   // MutableList<Int> will NOT fit
  • out on a type parameter means it only appears in output positions; in means only in input positions.
  • Kotlin declares variance at the declaration site, unlike Java's wildcards at each use site.
  • Use-site variance is still available when a type is invariant but you know how you will use it: Array<out Any>.
  • A List<T> in a function parameter is usually the wrong shape if you intend to add to it; ask for MutableList<T> or return a new list.

Bounds, projections and reified

fun <T : Comparable<T>> maxOfList(items: List<T>): T {
    require(items.isNotEmpty()) { "list must not be empty" }
    return items.reduce { a, b -> if (a > b) a else b }
}

// multiple bounds need a where clause
fun <T> merge(a: T, b: T): T where T : CharSequence, T : Appendable = a

// star projection: the element type is unknown, so you can only read Any?
fun size(items: List<*>) = items.size

// reified keeps the type at runtime, which requires inline
inline fun <reified T> List<*>.filterIsInstanceOf(): List<T> =
    filterIsInstance<T>()

// JSON-like tree that benefits from a sealed hierarchy rather than generics
sealed interface Node<out T> {
    data class Leaf<T>(val value: T) : Node<T>
    data class Branch<T>(val left: Node<T>, val right: Node<T>) : Node<T>
}
ConstructMeaningWhen to use
<out T>Covariant producerRead-only collections, results, factories
<in T>Contravariant consumerComparators, handlers, callbacks
<*>Unknown typeYou only need size or membership
reifiedType available at runtimeParsing, filtering by class, building maps

Delegation with by

interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println(message)
}

// implementation delegation: forward Logger to the delegate
class Service(logger: Logger) : Logger by logger {
    fun handle() {
        log("handling")          // no overriding boilerplate at all
    }
}

// property delegation
import kotlin.properties.Delegates

class Settings {
    var theme: String by Delegates.observable("light") { _, old, new ->
        println("theme changed from $old to $new")
    }

    var token: String? by Delegates.vetoable(null) { _, _, new ->
        new.isNullOrBlank().not()      // reject blank values
    }
}

// lazy is the most common delegation and is thread-safe by default
class Repository {
    val client: HttpClient by lazy { HttpClient() }
}
💡
A custom delegate is just an object with getValue and setValue operators. That is how most Android frameworks implement by viewModels() — storing the value in a map keyed by property so it survives configuration changes.

FAQ

Why can I pass a <code>List&lt;String&gt;</code> where <code>List&lt;Any&gt;</code> is expected?
Because List is declared with out E. It is read-only, so a list of strings is safely usable as a list of anything — you can only ever get values out of it.
When should I use <code>reified</code>?
When you need the actual class at runtime: filtering by type, deserialising, or building a map keyed by class. It forces inline, which increases bytecode size at each call site, so keep such functions small.

Functional Kotlin: lambdas, inline functions and DSLs Java interop, JVM tooling and build performance

Last refreshed 2026-09-18.