Sling Academy
Home/Kotlin/Kotlin - Understanding Variance in Generics: `in` and `out`

Kotlin - Understanding Variance in Generics: `in` and `out`

Last updated: December 05, 2024

Generics in Kotlin offer a way of defining flexible data structures and functions, allowing them to operate on any object type while ensuring type safety. Variance in generics comes into play when you want to control how different types are assigned in a hierarchy, particularly when you deal with complex data relationships. Kotlin offers two keywords: in and out, which are used to define variance, often compared to the PECS (Producer Extends, Consumer Super) rule in Java.

Covariance with Out

Covariance allows a type to be a subtype of another type. In Kotlin, this is achieved using the out keyword. When you declare a generic type as out, you restrict it to being able to produce output values, but not consume or read values. This is useful when dealing with read-only data.

interface Producer {  
    fun produce(): T
}

class MyProducer : Producer {
    override fun produce(): String {
        return "Hello from Kotlin!"
    }
}

fun main() {
    val stringProducer: Producer = MyProducer()
    val output: Any = stringProducer.produce()  
    println(output)
}

In this example, the interface Producer is covariant with respect to the type T. The out keyword indicates that Producer can safely be assigned to variables of Producer of its supertype, such as Any.

Contravariance with In

Contravariance, on the other hand, allows a type to consume objects of another type. By using the in keyword, you can declare a generic type to accept more generalized or supertype objects.

interface Consumer {
    fun consume(item: T)
}

class StringConsumer : Consumer {
    override fun consume(item: String) {
        println("Consumed: $item")
    }
}

fun main() {
    val stringConsumer: Consumer = StringConsumer()
    stringConsumer.consume("Kotlin is fun!")
}

By using Consumer<in T>, you specify the ability to consume items of type T, where T is a subtype. This allows a Consumer<String> to be passed to functions expecting Consumer<Any>.

Invariance

Invariance means that you cannot assign an object of a derived class to a reference of its base class when it is of different generic parameter values. This is different from normal class inheritance.

class Box<T>(val value: T)

fun main() {
    val stringBox: Box = Box("Hello")
    // val anyBox: Box = stringBox // This is not allowed
    println(stringBox.value)
}

Kotlin enforces type safety by preventing such assignments unless you explicitly handle variance with in or out.

The Benefits of Generics Variance

Understanding variance is an essential part of mastering Kotlin generics. Here are some benefits:

  • Type Safety: Helps avoid runtime errors by checking type compatibility at compile time.
  • Flexibility: Offers flexible APIs that can interact with various types, enhancing code reusability and consistency.
  • Expressiveness: Bring clarity in type declarations, reducing bugs and misinterpretations in the program logic.

In summary, leveraging in and out provides powerful tools in your Kotlin toolbox for designing clear, concise, and robust generic APIs. This guarantees that your functions will work reliably across different use-cases without unnecessary type-casting.

Next Article: Covariance and Contravariance Simplified in Kotlin

Previous Article: Creating Generic Functions in Kotlin

Series: Advanced Kotlin Features

Kotlin

You May Also Like

  • How to Use Modulo for Cyclic Arithmetic in Kotlin
  • Kotlin: Infinite Loop Detected in Code
  • Fixing Kotlin Error: Index Out of Bounds in List Access
  • Setting Up JDBC in a Kotlin Application
  • Creating a File Explorer App with Kotlin
  • How to Work with APIs in Kotlin
  • What is the `when` Expression in Kotlin?
  • Writing a Script to Rename Multiple Files Programmatically in Kotlin
  • Using Safe Calls (`?.`) to Avoid NullPointerExceptions in Kotlin
  • Chaining Safe Calls for Complex Operations in Kotlin
  • Using the Elvis Operator for Default Values in Kotlin
  • Combining Safe Calls and the Elvis Operator in Kotlin
  • When to Avoid the Null Assertion Operator (`!!`) in Kotlin
  • How to Check for Null Values with `if` Statements in Kotlin
  • Using `let` with Nullable Variables for Scoped Operations in Kotlin
  • Kotlin: How to Handle Nulls in Function Parameters
  • Returning Nullable Values from Functions in Kotlin
  • Safely Accessing Properties of Nullable Objects in Kotlin
  • How to Use `is` for Nullable Type Checking in Kotlin