You are currently viewing Kotlin Generics: How to Use and Understand Them

Kotlin Generics: How to Use and Understand Them

Generics are a powerful feature in Kotlin that allows you to write flexible and reusable code. By using generics, you can create classes, methods, and interfaces that work with any data type while maintaining type safety. This eliminates the need for casting and helps catch errors at compile time rather than at runtime.

In this guide, we will delve into the concept of generics in Kotlin. We will explore how to define and use generic classes and functions, understand variance (invariance, covariance, and contravariance), apply type constraints, and utilize reified type parameters. By mastering these concepts, you will be able to write more robust and versatile Kotlin code.

Basics of Generics

Generic Classes

A generic class in Kotlin is a class that can operate on a specified type parameter. This allows the class to be more flexible and reusable.

class Box<T>(var content: T)

fun main() {

    val intBox = Box(1)
    val stringBox = Box("Kotlin")

}

In this example, Box is a generic class with a type parameter T. The class can hold any type of content. We create two instances of Box, one holding an Int and the other holding a String.

Generic Functions

Generic functions are functions that can operate on generic types. They are defined with type parameters, similar to generic classes.

fun <T> display(content: T) {
    println(content)
}

fun main() {

    display(123)
    display("Hello, Kotlin!")

}

Here, display is a generic function with a type parameter T. It prints the content of any type passed to it. We call display with an Int and a String.

Variance in Generics

Invariance

By default, Kotlin generics are invariant, meaning that Box<Dog> is not a subtype of Box<Animal>, even if Dog is a subtype of Animal.

class Box<T>(var content: T)
open class Animal
class Dog : Animal()

fun main() {

    val animalBox: Box<Animal> = Box(Dog()) // This works
    // val dogBox: Box<Animal> = Box<Dog>(Dog()) // This does not compile

}

In this example, Box<Animal> and Box<Dog> are considered distinct types due to invariance.

Covariance

Covariance allows a generic type to be a subtype of another generic type. It is declared using the out keyword.

class Box<T>(var content: T)
open class Animal
class Dog : Animal()


fun main() {

    val animalBox: Box<Animal> = Box(Dog())

}

Here, Box is declared as covariant with out T. This means Box<Dog> is a subtype of Box<Animal>, allowing animalBox to hold a Box<Dog>.

Contravariance

Contravariance allows a generic type to be a supertype of another generic type. It is declared using the in keyword.

open class Animal
class Dog : Animal()

class Processor<in T> {

    fun process(value: T) {
        println(value)
    }

}

fun main() {

    val dogProcessor: Processor<Dog> = Processor<Animal>()

}

In this example, Processor is declared as contravariant with in T. This means Processor<Animal> is a supertype of Processor<Dog>, allowing dogProcessor to hold a Processor<Animal>.

Type Constraints

Upper Bounds

Type constraints allow you to restrict the types that can be used as type arguments. The most common type constraint is the upper bound, specified using the : symbol.

fun <T : Number> add(a: T, b: T): T {
    return a.toDouble().plus(b.toDouble()) as T
}

fun main() {

    val sum = add(1, 2)
    println(sum) // Output: 3.0

}

Here, the type parameter T is constrained to Number and its subclasses. The add function ensures that only numeric types are used.

Multiple Constraints

You can specify multiple type constraints using the where keyword.

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<T>
        where T : Comparable<T>, T : Any {
    return list.filter { it > threshold }
}

fun main() {

    val numbers = listOf(1, 2, 3, 4, 5)
    val filteredNumbers = copyWhenGreater(numbers, 3)
    println(filteredNumbers) // Output: [4, 5]

}

In this example, T is constrained to be both Comparable and non-nullable. The copyWhenGreater function filters the list based on the threshold.

Reified Type Parameters

Reified type parameters allow you to access the actual type arguments at runtime. This is achieved by using the reified keyword and inline functions.

inline fun <reified T> isType(value: Any): Boolean {
    return value is T
}

fun main() {

    println(isType<String>("Hello")) // Output: true
    println(isType<Int>("Hello")) // Output: false

}

Here, the isType function checks if a value is of a specified type. The reified keyword allows the type check to be performed at runtime.

Conclusion

Kotlin generics provide a powerful way to create flexible and reusable code. By understanding and utilizing generic classes, functions, variance (invariance, covariance, contravariance), type constraints, and reified type parameters, you can write more robust and type-safe Kotlin code. Generics help you avoid runtime errors and make your code more expressive and maintainable.

Additional Resources

To further your understanding of Kotlin generics, consider exploring the following resources:

  1. Kotlin Documentation: The official documentation for Kotlin. Kotlin Documentation
  2. Kotlin by JetBrains: Learn Kotlin through official JetBrains resources. Kotlin by JetBrains
  3. Kotlin for Android Developers: A comprehensive guide to using Kotlin for Android development. Kotlin for Android Developers
  4. Kotlin Koans: Interactive exercises to learn Kotlin. Kotlin Koans
  5. KotlinConf Talks: Watch talks from the Kotlin conference. KotlinConf Talks

By leveraging these resources, you can deepen your knowledge of Kotlin generics and enhance your ability to develop efficient and maintainable applications.

Leave a Reply