Generics in Kotlin: Why They Are Needed and How to Use Them in Practice

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

Generics in Kotlin: Why They Are Needed and How to Use Them in Practice



Generics are one of the key features of the Kotlin language, allowing you to write more flexible, type-safe, and reusable code. If you've ever written List<String> or Map<Int, User>, you've already encountered generics. But why are they actually needed, and how can you get the most out of them? In this article, we'll break down the theory and move on to practice with real-world examples.



The Problem That Generics Solve



Imagine you need to write a function that returns the first element from a list. Without generics, you would have to write a separate function for each data type:



fun getFirstString(list: List<String>): String? = list.firstOrNull()fun getFirstInt(list: List<Int>): Int? = list.firstOrNull()fun getFirstUser(list: List<User>): User? = list.firstOrNull()


This leads to code duplication. Using Any would solve the problem, but you would lose type information at compile time:



fun getFirst(list: List<Any>): Any? = list.firstOrNull()

// You would have to cast typesval name: String? = getFirst(strings) as? String


Generics solve this problem elegantly:



fun <T> getFirst(list: List<T>): T? = list.firstOrNull()

// The compiler knows the type!val name: String? = getFirst(listOf("Alice", "Bob"))val age: Int? = getFirst(listOf(25, 30))


Now one function works with any type while maintaining full type safety.



Basics of Generics in Kotlin: Syntax and Declaration



Generics are declared using angle brackets <T>. The letter T is just a convention; you can use any names: E (element), K (key), V (value).



Generic Classes



class Box<T>(val value: T) {    fun getContent(): T = value}

fun main() { val stringBox = Box("Hello") val intBox = Box(42) println(stringBox.getContent()) // Hello println(intBox.getContent()) // 42}


Generic Functions



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

// You can specify the type explicitlyprintItem<String>("Kotlin")// Or rely on type inferenceprintItem(100)


Generic Interfaces



interface Repository<T> {    fun getById(id: Int): T    fun save(item: T): Boolean}

class UserRepository : Repository<User> { override fun getById(id: Int): User { /* ... */ } override fun save(item: User): Boolean { /* ... */ }}


Variance: in, out, and the Star *



One of the most powerful features of Kotlin is variance. It answers the question: can List<String> be a subtype of List<Any>?



Covariance (out)



Use out when your generic type is only returned (producer). For example, List<T> is covariant:



// You can assign List<String> to a List<Any> variableval strings: List<String> = listOf("a", "b")val anyList: List<Any> = strings // OK!

// But you cannot add elements// anyList.add(1) // Compilation error!


Custom example:



class Producer<out T>(private val value: T) {    fun produce(): T = value}

fun main() { val stringProducer: Producer<String> = Producer("Hello") val anyProducer: Producer<Any> = stringProducer // Works! println(anyProducer.produce())}


Contravariance (in)



Use in when the type is only accepted (consumer). For example, Comparable<T>:





                
                

Blogs

Book Recommendations