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

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



When developing in Kotlin, you often encounter the need to write code that can work with different data types. Instead of creating separate functions or classes for Int, String, Double, and so on, you can use the Generics mechanism. This article explains what Generics in Kotlin are, why they are needed, and how to apply them in practice.



What are Generics and why are they needed?



Generics are a tool that allows you to write classes, interfaces, and functions that work with arbitrary data types. Imagine you are creating a box into which you can put anything: a book, a phone, or food. Instead of making a separate box for each item, you make one universal one.



The main advantages of using Generics:

  • Type Safety — the compiler checks types at compile time, preventing runtime errors.
  • Code Reusability — code written once works with any types.
  • Readability — the code becomes clearer because it explicitly indicates which types it works with.


Without Generics, you would have to use Any and constantly cast types, which is error-prone. With Generics, the compiler itself ensures type correctness.



Basics of Generics in Kotlin: type parameters



In Kotlin, Generics are implemented through type parameters. They are specified in angle brackets <>. The most commonly used letters are T (Type), E (Element), K (Key), V (Value), but any names can be used.



Generic functions



Let's start with a simple example — a function that returns the argument passed to it:



fun <T> identity(value: T): T {    return value}

fun main() { val number: Int = identity(42) val text: String = identity("Hello") println(number) // 42 println(text) // Hello}


Here, <T> before the function name declares the type parameter. The type T is inferred automatically from the argument, so you don't have to specify it explicitly.



Generic classes



Classes can also be generic. Let's create a simple container:



class Box<T>(private val item: T) {    fun getItem(): T {        return item    }}

fun main() { val stringBox = Box("Book") val intBox = Box(123)

println(stringBox.getItem()) // Book println(intBox.getItem()) // 123}


The class Box<T> can store an object of any type. When creating an instance, we specify the specific type, and the compiler guarantees that getItem() returns exactly that type.



Type Bounds



Sometimes you need to restrict the types that can be used in a Generic. For example, to make a function work only with numbers. This is done using bounds with the keyword :.



Upper Bound



fun <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 // println(sum("A", "B")) // Compilation error}


Here, T : Number means that the type T must be a subclass of the Number class. Int, Double, Float are suitable, while String is not.



Multiple bounds



If you need multiple bounds, use where:



interface Printable {    fun print()}

fun <T>

Blogs

Book Recommendations