Generics in Kotlin: why they are needed and how to use them

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction to Generics in Kotlin



Generics (generic types) are one of the key features of statically typed programming languages, including Kotlin. They allow you to write flexible, reusable code that works with different data types while maintaining strict type safety at compile time.



Imagine you need to create a data structure for storing a list of elements. Without generics, you would have to write separate classes for each type (IntList, StringList, UserList). With generics, a single class MyList<T> is enough, which will work with any type.



// Without Generics — you have to duplicate codeclass IntList(private val items: MutableList<Int>)class StringList(private val items: MutableList<String>)

// With Generics — one class for all typesclass GenericList<T>(private val items: MutableList<T>) { fun add(item: T) { items.add(item) } fun get(index: Int): T = items[index]}


In this article, we will thoroughly examine how generics work in Kotlin, look at their syntax, use cases, and advanced concepts like covariance and contravariance.



Basics of Working with Generics



Declaring Generic Classes and Functions



To declare a generic class, simply specify one or more type parameters in angle brackets after the class name:



class Box<T>(var value: T) {    fun getValue(): T = value    fun setValue(newValue: T) { value = newValue }}

// Usageval intBox = Box<Int>(42)val stringBox = Box<String>("Hello")

// Kotlin can infer the type, so you can write it shorterval anotherBox = Box("Auto type inference")


Functions can also be generic. The type parameter is specified before the function name:



fun <T> printItem(item: T) {    println("Item: $item")}

fun <T> createPair(first: T, second: T): Pair<T, T> { return Pair(first, second)}

// UsageprintItem(123)printItem("Text")val pair = createPair("A", "B")


Type Bounds



Sometimes you need to restrict the types that can be used with generics. For this, upper bounds are used with the keyword : Type:



// Only types that implement Comparablefun <T : Comparable<T>> findMax(a: T, b: T): T {    return if (a > b) a else b}

// Multiple constraints using wherefun <T> processItem(item: T) where T : CharSequence, T : Comparable<T> { println("Length: ${item.length}") println("Compared: ${item.compareTo(item)}")}

// Usageprintln(findMax(10, 20)) // 20println(findMax("Apple", "Banana")) // "Banana"// findMax(10, "Text") // Compilation error!


Variance: in, out and Type Projections



One of the most complex but important topics in generics is variance. It defines how generic types relate to each other during inheritance.



Invariance (default)



By default, generics in Kotlin are invariant. This means that Box<Number> and Box<Int> are not subtypes of each other, even though Int is a subtype of Number:



val intBox: Box<Int> = Box(42)// val numBox: Box<Number> = intBox // Compilation error!


Covariance (out)



The out modifier makes the type covariant. This means that Box<Int> becomes a subtype of Box<Number>. It is used when type T is only used in output positions (return values):



class Producer<out T>(private val item: T) {    fun produce(): T = item  // T only in output}                

Blogs

Book Recommendations