johnburnsonline.com

Demystifying Variance in Kotlin Generics: A Comprehensive Guide

Written on

Understanding Variance in Kotlin Generics

Have you ever ventured into Kotlin generics and found yourself puzzled by the terms "in" and "out"? You're certainly not the only one! These terms, referred to as variance modifiers, can initially seem perplexing. However, this guide will clarify their roles, enabling you to craft more adaptable and reusable code.

The Concept of Variance Through Weaponry

Let's visualize a scenario involving weaponry, where we establish a hierarchy of classes:

open class Weapon

open class Rifle : Weapon()

class SniperRifle : Rifle()

Now, we aim to create a generic container class named Case to store these weapons. The question arises: how do we manage various weapon types? This is where the concept of variance becomes essential!

The Power of "out": The Producer

The out keyword signifies a covariant type. You can think of covariance as a producer. A covariant generic type is designed to return values, but it cannot accept them as inputs.

Consider this Case class utilizing out:

class Case<out T> {

private val contents = mutableListOf<T>()

fun produce(): T = contents.last() // Producer: OK

fun consume(item: T) = contents.add(item) // Consumer: Error!

}

In this Case class, you can produce any weapon of type T or its subtypes (like Rifle or SniperRifle), but you cannot add items (consume) because the generic type T could be something unexpected.

The elegance of out lies in its ability to utilize subtyping. Since SniperRifle is a subtype of Rifle, a Case can be used in any context where a Case is expected!

fun useProducer(case: Case<Rifle>) {

val rifle = case.produce() // Produces Rifle and its subtypes

}

useProducer(Case<Rifle>()) // OK, subtyping preserved

useProducer(Case<SniperRifle>()) // OK

useProducer(Case<Weapon>()) // Error, not a subtype of Rifle

This approach enhances the reusability of your code, but keep in mind that Case is read-only.

The Role of "in": The Consumer

The in keyword represents a contravariant type. Contravariance denotes a consumer. A contravariant generic type can only accept arguments, but it cannot return them.

Here’s a Case class that employs in:

class Case<in T> {

private val contents = mutableListOf<T>()

fun produce(): T = contents.last() // Producer: Error!

fun consume(item: T) = contents.add(item) // Consumer: OK

}

In this instance, Case can accept any weapon of type T or its supertypes (like Rifle or Weapon). The subtyping relationship is inverted here, making Case a subtype of Case, which allows you to add any weapon type into it.

fun useConsumer(case: Case<Rifle>) {

case.consume(SniperRifle()) // Consumes Rifle and its subtypes

}

useConsumer(Case<Weapon>()) // Error, not a supertype of Rifle

useConsumer(Case<Rifle>()) // OK

useConsumer(Case<Rifle>()) // OK, subtyping reversed

This method offers flexibility for consuming various weapon types, but note that Case becomes write-only.

The Invariant Approach: Producer and Consumer

Without variance modifiers, we encounter an invariant generic type. This type can both produce and consume values of type T.

class Case<T> {

private val contents = mutableListOf<T>()

fun produce(): T = contents.last() // Producer: OK

fun consume(item: T) = contents.add(item) // Consumer: OK

}

This approach is the most restrictive, as it does not permit subtyping for the generic type.

fun useProducerConsumer(case: Case<Rifle>) {

case.produce()

case.consume(SniperRifle())

}

useProducerConsumer(Case<Rifle>()) // Error, no subtyping

useProducerConsumer(Case<Rifle>()) // OK

useProducerConsumer(Case<Weapon>()) // Error, no subtyping

While it provides read-write access, it lacks the flexibility offered by out and in.

Conclusion

Understanding variance in Kotlin generics introduces a significant level of control and flexibility to your coding practices. By grasping the concepts of out, in, and invariant types, you can develop generic classes that are not only more reusable but also more expressive. Remember, out transforms your class into a producer (read-only), in makes it a consumer (write-only), and without variance modifiers, it's both a producer and consumer (read-write but less adaptable).

Recognizing when to apply each method empowers you to write cleaner and more maintainable Kotlin code. Therefore, the next time you encounter generics, don’t shy away from in and out—embrace them as tools to elevate your Kotlin expertise!

The first video titled "Variance... without Generics!" delves into the fundamental concepts of variance in Kotlin, offering insights into how it operates without the complexity of generics.

The second video, "Advanced Kotlin: Generics, Type Erasure, and Reflection Explained," provides a thorough exploration of advanced topics in Kotlin, including generics and their implications in programming.

Share the page:

Twitter Facebook Reddit LinkIn

-----------------------

Recent Post:

How Stoicism and Modern Psychology Illuminate Daily Life Clarity

Explore how Stoicism and modern psychology can enhance clarity and balance in our daily lives.

Xiaomi's CyberOne: The Next Step in Humanoid Robotics

Xiaomi's CyberOne humanoid robot showcases advanced AI and emotional recognition, competing with Tesla's Teslabot for a future in robotics.

Innovative Autonomous Landing System to be Tested by NASA

NASA prepares to test an advanced autonomous landing system with Blue Origin's New Shepard rocket, enhancing future space missions.

Unraveling How Certain Cancer Cells Evade Chemotherapy

This article explores how some cancer cells resist chemotherapy, revealing insights from recent research and implications for future treatments.

# Life's Profound Lessons from Living Well: Insights to Cherish

Discover three invaluable lessons a well-lived life imparts, highlighting the long-term view, self-comparison, and deeper realities.

Unlocking Savings: How Technology Can Help You Economize

Discover how technology can help you save money in various aspects of life, from internet access to automation.

Embracing the Journey of Suffering for Personal Growth

Exploring the paradox of seeking suffering for growth and understanding its role in our lives.

Revitalize Your Lifestyle: Embrace Seasonal Changes for Growth

Discover the importance of aligning with seasonal changes to enhance your well-being and find balance in life.