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 are one of the key features of the Kotlin language, allowing you to write flexible, type-safe, and reusable code. If you've ever written List<String> or Map<Int, User>, you've already used generics without even thinking about it. But what lies behind these angle brackets and how can you get the most out of them? In this article, we'll explore why Generics are needed in Kotlin, how to apply them in practice, and what advanced features (variance, reified, where) will help you write even cleaner and safer code.



This article is intended for developers who are already familiar with the basics of Kotlin and want to deepen their knowledge. We'll cover everything from basic syntax to real-world examples from production code.



Why are Generics needed? The problem without them



Imagine you need to write a function that takes a list of any elements and returns the first element. Without generics, you would write it like this:



fun getFirst(list: List<Any>): Any {    return list[0]}


Now, if you pass a List<String>, you'll get an Any as output. To work with it as a string, you'll have to do an explicit type cast:



val first = getFirst(listOf("Kotlin", "Java")) as Stringfirst.length // Dangerous! May throw ClassCastException


This is inconvenient and unsafe. The error will only manifest at runtime. Generics solve this problem by preserving type information at compile time.



Main advantages of Generics:

  • Type safety — the compiler checks types at compile time, not at runtime.
  • Elimination of explicit casts — code becomes cleaner and more readable.
  • Reusability — one class or function works with different types.


Basic syntax: classes, functions, and constraints



Generic classes



The simplest way to start is to declare a class with one or more type parameters. Type parameters are specified in angle brackets <T> (the letter can be anything, but traditionally T, E, K, V are used):



class Box<T>(val value: T)

fun main() { val stringBox = Box("Kotlin") // Type is inferred automatically val intBox = Box(42) // Box<Int>

println(stringBox.value.length) // Works without casting! println(intBox.value + 1) // 43}


Kotlin can infer the type, so writing Box<String> is not necessary — Box("Kotlin") is sufficient.



Generic functions



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



fun <T> getMiddleElement(list: List<T>): T? {    if (list.isEmpty()) return null    return list[list.size / 2]}

fun main() { val numbers = listOf(1, 2, 3, 4, 5) val middle = getMiddleElement(numbers) // Type Int? is inferred automatically println(middle) // 3}


Constraints (where / upper bound)



Sometimes you need to restrict which types can be used in a generic. For example, to call the .compareTo() method, the type must implement the Comparable interface:



fun <T : Comparable<T>> maxOfTwo(a: T, b: T): T {    return if (a >= b) a else b}

fun main() { println(maxOfTwo(10, 20)) // 20 println(maxOfTwo("A", "B")) // B // maxOfTwo(10, "A") // Compilation error: types don't match}


If you need multiple constraints, use the where keyword:



interface Printable {    fun print()}

fun <T> process(item: T) where T : Comparable<T>, T : Printable

Blogs

Book Recommendations