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>