Generic classes declare one or more type parameters (e.g. T) so the class can work with any type while keeping compile-time type safety — avoiding casts and runtime errors.
Generic class Boxclass Box<T>(var value: T) {
fun get(): T = value
fun set(item: T) {
value = item
}
}
fun main() {
val intBox = Box(42) // Box<Int> — type inferred
val stringBox = Box("Kotlin") // Box<String>
println(intBox.get()) // 42
println(stringBox.get()) // Kotlin
}
Multiple type parameters
Pair-like generic classclass PairHolder<A, B>(val first: A, val second: B) {
fun swap(): PairHolder<B, A> {
return PairHolder(second, first)
}
}
fun main() {
val pair = PairHolder("Name", 100)
val swapped = pair.swap()
println("${swapped.first}, ${swapped.second}") // 100, Name
}
Why generics matter
Without generics
With generics
Box stores Any? — need casts
Box<T> — type known at compile time
Runtime ClassCastException risk
Compiler catches type mismatches
Less readable API
Self-documenting: List<User>
Standard library examplesval names: List<String> = listOf("Kotlin", "Java")
val cache: MutableMap<String, Int> = mutableMapOf()
val ids: Result<Int> = Result.success(42)
Type erasure
On the JVM, generic type parameters are erased at runtime — List<String> becomes List. Use reified types with inline functions when you need runtime type access.
2Generic Functions
A generic function has its own type parameters independent of the enclosing class. Declare them before the function name with angle brackets.
Standalone generic functionfun <T> singletonList(item: T): List<T> {
return listOf(item)
}
fun <T> T.applyTwice(block: (T) -> T): T {
return block(block(this))
}
fun main() {
println(singletonList("Hello")) // [Hello]
println(5.applyTwice { it * 2 }) // 20
}
Generic functions inside classes
Class with generic methodclass Utils {
fun <T> printAll(items: List<T>) {
items.forEach { println(it) }
}
fun <T> firstOrNull(items: List<T>): T? {
return items.firstOrNull()
}
}
fun main() {
Utils().printAll(listOf(1, 2, 3))
println(Utils().firstOrNull(listOf("a", "b"))) // a
}
Type constraints limit which types can be used as type arguments — so you can safely call methods on T that belong to a specific superclass or interface.
Upper bound with :
T must extend Numberfun <T : Number> sum(a: T, b: T): Double {
return a.toDouble() + b.toDouble()
}
fun main() {
println(sum(10, 20)) // 30.0
println(sum(3.5, 2.5)) // 6.0
// sum("a", "b") // Compile error — String is not a Number
}
Multiple constraints with where
where clausefun <T> max(a: T, b: T): T
where T : Comparable<T> {
return if (a >= b) a else b
}
fun main() {
println(max(10, 25)) // 25
println(max("apple", "banana")) // banana
}
Class with where constraintsclass SortedList<T> : Iterable<T>
where T : Comparable<T> {
private val items = mutableListOf<T>()
fun add(item: T) {
items.add(item)
items.sort()
}
override fun iterator(): Iterator<T> = items.iterator()
}
Constraint syntax
Meaning
<T : Number>
T must be Number or subclass
<T : Comparable<T>>
T must be comparable to itself
where T : A, T : B
T must implement both A and B
Default upper bound
Any? if none specified
Nullable type param
By default T can be nullable. Use T : Any to require a non-null type argument.
4Variance
Variance describes how subtyping between generic types relates to subtyping of their type arguments. Kotlin uses declaration-site variance with out and in keywords.
Covariance — out (producer)
out T means T appears only in out (return) positions — the type is a producer of T. If Dog is a subtype of Animal, then List<Dog> can be treated as List<out Animal>.
Covariant interfaceinterface Producer<out T> {
fun produce(): T
}
class DogProducer : Producer<Dog> {
override fun produce() = Dog("Buddy")
}
open class Animal(val name: String)
class Dog(name: String) : Animal(name)
fun feedAll(producer: Producer<Animal>) {
val animal: Animal = producer.produce()
println("Feeding ${animal.name}")
}
fun main() {
feedAll(DogProducer()) // OK — Producer<Dog> is subtype of Producer<Animal>
}
Contravariance — in (consumer)
in T means T appears only in in (parameter) positions — the type is a consumer of T.
Contravariant interfaceinterface Consumer<in T> {
fun consume(item: T)
}
class AnimalConsumer : Consumer<Animal> {
override fun consume(item: Animal) {
println("Consumed ${item.name}")
}
}
fun feedDog(consumer: Consumer<Dog>) {
consumer.consume(Dog("Max"))
}
fun main() {
feedDog(AnimalConsumer()) // OK — Consumer<Animal> is subtype of Consumer<Dog>
}
Variance summary
Keyword
Name
T used as
Subtyping
out T
Covariant
Output (return) only
Preserves subtyping direction
in T
Contravariant
Input (parameter) only
Reverses subtyping direction
T (none)
Invariant
Both in and out
No subtyping — exact match
List is covariant (read-only)// List<out E> in Kotlin — you can assign:
val dogs: List<Dog> = listOf(Dog("Rex"))
val animals: List<Animal> = dogs // OK for read-only List
// MutableList is invariant — cannot assign MutableList<Dog> to MutableList<Animal>
PECS mnemonic:Producer Extends → out; Consumer Super → in. Kotlin builds this into declarations instead of use-site wildcards like Java's ? extends / ? super.
5Reified Types
Normally JVM type erasure removes generic type info at runtime. Kotlin's reified type parameters — used with inline functions — preserve the actual type at runtime so you can use T::class, is T, and reflection without passing Class<T> manually.
reified type parameterinline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
inline fun <reified T> List<*>.filterIsInstance(): List<T> {
return filterIsInstance<T>()
}
fun main() {
println(isInstance<String>("hello")) // true
println(isInstance<Int>("hello")) // false
val mixed = listOf(1, "two", 3, "four")
val strings = mixed.filterIsInstance<String>()
println(strings) // [two, four]
}
Requirements for reified
Requirement
Reason
Must be inline fun
Compiler copies bytecode per actual type
reified keyword on type param
Marks T as preserved at call site
Cannot use on non-inline functions
Erasure applies at runtime otherwise
Cannot use on class type params
Only inline function type parameters
Practical reified examples
startActivity pattern (Android-style)inline fun <reified T> createInstance(): T {
return T::class.java.getDeclaredConstructor().newInstance()
}
// Simplified intent-style navigation concept:
inline fun <reified T> navigateTo(): String {
return "Opening ${T::class.simpleName}"
}
fun main() {
println(navigateTo<String>()) // Opening String
}