Type Parameters
<T> ยท generic functions ยท generic classesA generic type parameter lets you write a function or class once and have it work correctly for any type, without giving up compile-time type checking the way a raw Any parameter would. <T> is a placeholder the caller fills in โ either explicitly, or inferred from the arguments โ and every use of T inside the body is checked as if it were a real type, because by the time the compiler is done, it is one.
// T is inferred from the argument at each call site fun <T> firstOrNull(list: List<T>): T? { return if (list.isEmpty()) null else list[0] } println(firstOrNull(listOf(1, 2, 3))) // 1 โ T inferred as Int println(firstOrNull(listOf("a", "b"))) // a โ T inferred as String println(firstOrNull<Int>(emptyList())) // null โ T given explicitly, nothing to infer from
// The class itself is parameterized: every instance fixes T once class Box<T>(var value: T) { fun <R> map(transform: (T) -> R): Box<R> = Box(transform(value)) } val intBox = Box(42) // Boxval stringBox = intBox.map { it.toString() } // Box println(stringBox.value) // "42"
func First[T any](s []T) T and Kotlin's fun <T> firstOrNull(list: List<T>): T? solve the same problem the same way. The syntax difference is just bracket placement โ Kotlin's <T> comes before the function name, Go's after.Declaration-Site Variance: out / in
out (producer) ยท in (consumer)Without variance annotations, Box<Dog> and Box<Animal> are unrelated types even if Dog is a subtype of Animal โ generics are invariant by default, for a good reason covered below. Kotlin lets you opt into a safer form of subtyping directly on the type parameter's declaration: out marks a type parameter as covariant (only ever produced, read out, never accepted as input), and in marks it as contravariant (only ever consumed, accepted as input, never returned).
out T (covariant โ T only ever comes OUT)
Producer<Dog> is a subtype of Producer<Animal>
(a supply of Dogs can stand in for a supply of Animals)
in T (contravariant โ T only ever goes IN)
Consumer<Animal> is a subtype of Consumer<Dog>
(something that can handle any Animal can handle a Dog)
Neither out nor in T (invariant โ T is used both ways)
Box<Dog> and Box<Animal> are unrelated types
open class Animal class Dog : Animal() // out T: instances only ever return T, never accept it as a param interface Producer<out T> { fun produce(): T } class DogProducer : Producer<Dog> { override fun produce() = Dog() } // Legal because of `out`: a Producercan stand in for Producer val producer: Producer<Animal> = DogProducer() val animal: Animal = producer.produce()
// in T: instances only ever accept T as a parameter, never return it interface Consumer<in T> { fun consume(item: T) } class AnimalConsumer : Consumer<Animal> { override fun consume(item: Animal) = println("consumed an animal") } // Legal because of `in`: something that consumes any Animal // can be used wherever a Consumeris needed val consumer: Consumer<Dog> = AnimalConsumer() consumer.consume(Dog())
List<out T> is covariant, because a read-only list only ever produces its elements โ this is exactly why List<String> can be passed where List<Any> is expected. MutableList<T> has no variance annotation, because add(item: T) both accepts and returns T, which is unsafe to treat as covariant (see why, next).Why Invariance Is the Safe Default
Java's array mistake ยท compile-time safetyObject[] arr = new String[3] compiles), and it's a well-known design mistake: nothing stops you from then writing arr[0] = 42, which compiles but throws ArrayStoreException at runtime. Kotlin generics are invariant unless you explicitly opt into out or in, precisely so the compiler โ not a runtime exception โ is what catches an unsafe substitution.open class Animal class Dog : Animal() class Cat : Animal() // If MutableListwere allowed (it isn't โ this is illustrative): // val dogs: MutableList= mutableListOf(Dog()) // val animals: MutableList= dogs // pretend this compiled // animals.add(Cat()) // adding a Cat to a "List"! // val d: Dog = dogs[1] // ClassCastException at runtime // This is exactly why MutableListhas no variance annotation: // the compiler refuses the middle assignment outright
[]Dog and []Animal are simply unrelated types with no subtyping relationship, full stop. Kotlin's variance annotations let you selectively unlock the subtyping Go never offers, but only where it's provably safe.Use-Site Variance & Star Projection
out at call site ยท in at call site ยท *Sometimes a type wasn't declared with out or in โ maybe you don't own it, or it genuinely needs to be invariant in general โ but at one specific call site, you only intend to read from it or only write to it. Use-site variance (also called type projection) lets you annotate the variance at the point of use instead of the declaration. Star projection, <*>, goes further: it means "some specific type, but I don't know or care which," useful when you need to accept any parameterization of a generic type without touching its contents.
// MutableListis invariant, but here we only ever read from source // and only ever write to destination โ project the variance locally fun <T> copy(source: MutableList<out T>, destination: MutableList<in T>) { for (item in source) { destination.add(item) } } val ints: MutableList<Int> = mutableListOf(1, 2, 3) val numbers: MutableList<Number> = mutableListOf() copy(ints, numbers) // legal: source is read-only here, destination write-only println(numbers) // [1, 2, 3]
// List<*> means "a List of some type, we don't know which" // Reading elements gives Any? (safe: some unknown type IS an Any?) // Writing is forbidden (unsafe: we don't know what type is expected) fun printAll(list: List<*>) { for (item in list) { println(item) // item: Any? } } printAll(listOf(1, 2, 3)) printAll(listOf("a", "b")) // Common use: checking type/size without caring about the element type fun isEmpty(list: List<*>) = list.isEmpty()
List<*> is still fully type-checked โ you just don't know the specific element type at that point, so the compiler restricts you to operations that are safe regardless of what it turns out to be (reading as Any?, but never writing a specific value in).reified with inline
reified ยท inline fun ยท T at runtimeNormally you can't do x is T or T::class inside a generic function, because type erasure (next section) means T doesn't exist as a real type at runtime โ the JVM has already thrown it away. reified is Kotlin's escape hatch: it's only legal on an inline function's type parameter, because inlining pastes the function body โ including a fully-known T from the call site โ directly into the caller, so there's no erasure to fight.
// Without reified, `x is T` and `T::class` wouldn't compile here โ // reified requires the function to be inline inline fun <reified T> isInstance(x: Any?): Boolean = x is T println(isInstance<String>("hi")) // true println(isInstance<Int>("hi")) // false // A very common real use: a typed JSON deserializer, avoiding // having to pass Classor a TypeToken around manually inline fun <reified T> parseJson(json: String): T { return Gson().fromJson(json, T::class.java) } val user: User = parseJson(jsonString) // T inferred as User from the target type
filterIsInstance<T>() and Android's getSystemService<T>() extensions โ anywhere a Java API needed you to pass a Class<T> object as a workaround for erasure, a reified inline wrapper lets Kotlin callers just write the type parameter and skip the extra argument entirely.Type Erasure
JVM bytecode ยท unchecked castKotlin generics, like Java's, exist only at compile time. Once compiled to JVM bytecode, List<String> and List<Int> are both just List โ the type argument is erased. This is a JVM platform constraint Kotlin inherited, not a design choice Kotlin made; it's why reified needs the inline workaround above, and why certain generic type checks are restricted or produce a compiler warning instead of an error.
// This does not compile: T isn't available at runtime to check against fun <T> isTypeOf(x: Any?): Boolean { // return x is T // ERROR: cannot check for erased type T return false } // Checking the outer type works fine; the type argument is unchecked val list: Any = listOf(1, 2, 3) if (list is List<*>) { // fine: star projection acknowledges the erasure println("it's a list") } // if (list is List) { } // warning: "unchecked cast", can't verify element type
reified only works through inline: inlining is the one mechanism that gets the actual type back into scope at the call site, before erasure has a chance to throw it away during compilation to bytecode.Generic Constraints
upper bound ยท where ยท multiple boundsBy default a type parameter is bounded only by Any? โ it could be anything, including null. An upper bound narrows that, restricting T to a specific type or its subtypes, which is what lets you call that type's members directly on values of type T inside the function body. A where clause lets you require more than one bound at once.
// T : Comparablerestricts T to comparable types, // which is what makes compareTo callable below fun <T : Comparable<T>> max(a: T, b: T): T = if (a.compareTo(b) >= 0) a else b println(max(3, 7)) // 7: Int implements Comparableprintln(max("apple", "banana")) // "banana": String implements Comparable // max(User("Ada"), User("Grace")) // ERROR unless User implements Comparable // Without a bound, T defaults to Any?, and .compareTo wouldn't exist: // fun max(a: T, b: T) = if (a.compareTo(b) >= 0) a else b // ERROR
interface Named { val name: String } // T must satisfy BOTH constraints: ComparableAND Named fun <T> describeLargest(items: List<T>): String where T : Comparable<T>, T : Named { val largest = items.maxOrNull() ?: return "empty" return "Largest: ${largest.name}" // .name available thanks to the Named bound }
func Max[T constraints.Ordered](a, b T) T. Kotlin's T : Comparable<T> upper bound and a where clause for multiple constraints are the direct equivalent of Go's type-parameter constraint interfaces โ same purpose, different syntax.Quick Reference
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| Generic function | fun <T> f(x: T): T | T inferred from arguments, or given explicitly |
| Generic class | class Box<T>(val v: T) | Each instance fixes T once |
| Covariant (declaration-site) | interface P<out T> | T only produced; Producer<Dog> : Producer<Animal> |
| Contravariant (declaration-site) | interface C<in T> | T only consumed; Consumer<Animal> : Consumer<Dog> |
| Invariant (default) | class Box<T> | No subtyping between Box<Dog> and Box<Animal> |
| Use-site projection | fun f(x: MutableList<out T>) | Local read-only view of an invariant type |
| Star projection | List<*> | Unknown type argument; read as Any?, can't write |
| Reified type parameter | inline fun <reified T> f() | Only legal on inline functions; enables `is T`, `T::class` |
| Type erasure | List<String> โ List at runtime | Type arguments don't exist in compiled bytecode |
| Upper bound | <T : Comparable<T>> | Restricts T, enables calling Bound's members on it |
| Multiple bounds | <T> ... where T : A, T : B | T must satisfy every listed constraint |