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? StringGenerics 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
Read also
How to Make Friends with C++ and Neural Networks: Writing a Client for the ChatGPT API
Containerizing C++ Applications with Docker: A Complete Guide
Deploying Java on Kubernetes: A Practical Basic Guide
Generating Random Integers in JavaScript: Precise Range in 2025
Generics and Typing in Swift: How to Write Code That Works for You