Variance In, Out, and Star Projections in Kotlin Language

Introduction to Variance In, Out, and Star Projections in Kotlin Language

The type management of how types interact with each other can be very powerful while working with generics. Probably, an important concept for anyone working with generics is the idea

of variance, the behavior of types when given as function arguments or used in data structures like lists, maps, or in your own classes. Variance in Kotlin is controlled using the in, out and star projections. The knowledge of these concepts helps in making type safety but at the same time provides flexibility for code reuse. We’ll begin with a deep dive into variance within Kotlin, using in and out keywords, and detailing special cases of variance: star projections. We’ll discuss when and how to apply these concepts using examples to help explain things.

What Is Variance in Kotlin Language?

Variance is the relation between types in a hierarchy, particularly when talking about generic types. Suppose in Kotlin we wanted to determine if, given Cat is a subtype of Animal, then List is a subtype of List. These answers are provided by variance, formulating a set of rules pertaining to how to proceed with type substitution when dealing with generics.

There are three main types of variance:

  1. Covariance (represented by out)
  2. Contravariance (represented by in)
  3. Invariance (no modifier)

Covariance (out Keyword)

Covariance lets a generic type preserve the same type relationship between types. Less formally, if Cat is a subtype of Animal, then List may be used anywhere List is required. This behavior is handy when a type is only producing values-that is, it’s an output.
Syntax and Use Case

Out keyword in Kotlin. It specifies a fact that the type parameter only produces values, therefore can be used only as a return type but not as a parameter type.

class Box<out T>(val value: T)

fun takeAnimalBox(animalBox: Box<Animal>) {
    println(animalBox.value)
}

fun main() {
    val catBox = Box(Cat())
    takeAnimalBox(catBox)  // Works because Box<Cat> is a subtype of Box<Animal>
}

It is covariant in Box< out T> here. That is, Box< Cat> can be passed wherever Box<Animal> is expected. It’s safe because the function just retrieves (or outputs) T from the box and does not modify or input any values of type T.

When to Use Covariance in Kotlin Language?

Use out when:

  • A generic class or interface is producing or returning values.
  • The type is used only in return position, never as input.

collections producing items like List<T> are good candidates of covariance if we want to read from the list but not modify it.

Contravariance (in Keyword)

Contravariance used for reversing the types relationship, allowing a generic type to take the reverse perspective. If we assume that the Cat is a subtype of Animal, then a Cage<Animal> would be usable wherever it would have an expectation of Cage<Cat>. This works in the case of a generic type wherein only consuming values are involved, in other words, it’s an input.

Syntax and Use Case

Contravariance in Kotlin This means a type parameter using in keyword means it is used only for input not for output.

class Cage<in T> {
    fun putInCage(item: T) { 
        // accepts item of type T
    }
}

fun putCatInCage(cage: Cage<Cat>) {
    cage.putInCage(Cat())
}

fun main() {
    val animalCage = Cage<Animal>()
    putCatInCage(animalCage)  // Works because Cage<Animal> is a supertype of Cage<Cat>
}

Here, Box is a covariant type. It means that Box can be passed wherever Box is expected. This is safe because the function simply retrieves (or returns) T from the box and does nothing about inputting any value of type T.

When to Use Covariance in Kotlin Language?

Use out when:

  • A generic class or interface is producing or returning values.
  • The type is used only in a return position, never as an input.

For example, we can define contravariant collections that would allow us to define values like List, which are suitable for our purposes only if we wanted to read from the list without modifying it.

Contravariance (in Keyword)

Contravariance lets a generic type flip the arrow of types. Suppose Cat is a subtype of Animal. Then a Cage should be usable everywhere where a Cage is used. This happens when the generic type is an exclusive consumer of values or, in other words, is an input.

Syntax and Use Case

The in keyword in Kotlin expresses contravariance. That is, a type parameter is used as an input but not as an output.

class Cage<in T> {
    fun putInCage(item: T) { 
        // accepts item of type T
    }
}

fun putCatInCage(cage: Cage<Cat>) {
    cage.putInCage(Cat())
}

fun main() {
    val animalCage = Cage<Animal>()
    putCatInCage(animalCage)  // Works because Cage<Animal> is a supertype of Cage<Cat>
}

In this example, Cage<in T> is a contravariant type. It allows Cage<Animal> to be used where Cage<Cat> is expected because the function only inputs values of type T into the cage.

When to Use Contravariance in Kotlin Language?

Use in when:

  • A generic class or interface is consuming values (e.g., function parameters).
  • The type is used as an input parameter, but you don’t need to return it.

For example, event handlers or consumer-like classes are often good candidates for contravariance.

Invariance (No in or out Keyword)

When neither in nor out is used, the type is invariant, meaning that the type must match exactly. For example, List<Cat> is not a subtype of List<Animal> and vice versa. This behavior is strict and doesn’t allow for any flexibility with type hierarchies.

Example of Invariance

class Container<T>(var item: T)

fun useContainer(container: Container<Animal>) {
    // some operations on container
}

fun main() {
    val catContainer = Container(Cat())
    // useContainer(catContainer) // This will cause an error because Container<Cat> is not a subtype of Container<Animal>
}

In this case, because Container<T> is invariant, Container<Cat> cannot be passed where Container<Animal> is expected, even though Cat is a subtype of Animal. Invariance ensures stricter type matching.

Star Projections (*)

Star projections (*) in Kotlin are used when you don’t know or don’t care about the exact type parameter, but you still want to use the generic type. It’s a way to handle unknown types safely by allowing both reading and writing operations within certain limitations.

Example of Star Projection

Consider the following generic class:

class Box<T>(val item: T)

Now, if we don’t know the exact type T, we can use a star projection:

fun printItem(box: Box<*>) {
    println(box.item)  // Can only read from the box, not write into it
}

fun main() {
    val intBox = Box(123)
    val stringBox = Box("Hello, Kotlin!")
    
    printItem(intBox)     // Output: 123
    printItem(stringBox)  // Output: Hello, Kotlin!
}

Behaviour of Star Projections

  • That is, we get a reference to an item but do not know what its type is, and we are treated to Any?.
  • You cannot alter the item in a Box<*> because Kotlin doesn’t know what type T is, which assures it is type-safe.

Star projections are really convenient when using code to work over generics, but you don’t need to know or to control the exact type. Such usage is very typical for APIs when flexibility in work with various types of generic is obligatory.

Practical cases of using star projections

Star projections are when you use it in cases where the method is to be applied on collections or generic types with any generic type parameter not specified. For example in reflection APIs, libraries or generic utility functions.

fun processList(list: List<*>) {
    list.forEach { println(it) }  // Safe to read
    // list.add("new item") // Error: Cannot add item because the exact type is unknown
}

Advantages of Variance In, Out, and Star Projections in Kotlin Language

The variance modifiers, in and out of Kotlin, together with star projections, offer one robust techniques for enhancing the flexibility of the use of generics while maintaining type safety in cases of complex hierarchies and type relationships. They prove particularly useful when handling covariant and contravariant types. Among the main benefits of variance and star projections in Kotlin are:

1. Enhanced Type Safety

The type variance modifiers, in and out, in Kotlin make sure that the type safety is ensured by explicitly defining whether a type parameter is for input (contravariant, in) or for output (covariant, out). With these, there is a reduced chance of runtime type errors.

  • Covariance (out): This allows you to use a subtype of a generic type instead of the supertype, which is the case when you can have read-only access to the object for instance, in a producer roles.
  • Contravariance (in): This lets you use a supertype, meaning you tend to write functions that work with the more general types, and it ensures that the code you are writing is flexible as well as safe.

2. More Flexible API Design

This facility of variance allows the API designers more flexibility in designing libraries that handle complex type hierarchies. Also, it allows an API to work smoothly with both more specific and more general types.

  • Fewer generic API overloads: When specifying variance, developers can ensure that their API works over a wider range of types without sacrificing type safety and thereby could potentially reduce overloads and improve the reusability of the code.
  • Less type casting: variance removes explicit type casts in working with different levels of type hierarchies, thus making the code cleaner and easier to maintain.

3. Better Interoperability with Java

Kotlin’s variance system works well with that way of wildcard usage in Java (? extends and ? super) and therefore there is better interoperability when Kotlin code interacts with the Java libraries or legacy code.

  • Seamless Interoperability: The out and in variance modifiers of Kotlin directly map to Java’s?extends and?super wild cards, thus making inter-operability between the two easier.
  • Clear semantics: That the explicit use of variance makes it easier to reason about how Kotlin types interact with Java generics, thereby removing the possibility of assumptions that may lead to a wrongly concluded type error

4. Improved Reusability

Variance makes the generic types in Kotlin more reusable, so they would ensure to work across a wider set of types without the possibility of violating type safety. This is particularly useful when dealing with more complex data structures.

  • Generic functions and classes: Using variance, generic functions and classes can be reused in a variety of contexts without having to define them for a certain subtype or supertype.
  • Reusable Components: After they are defined, data structures like List or Function can then be used anywhere in a program, without knowing the concrete types that are going to be used.

5. Expressive Type Projections with Star Projections (*)

Star projections in Kotlin are helpful for making a declaration that some generic type can be used as any possible type parameter without needing to declare the actual type. This is helpful for making the code flexible and for dealing with unknown types.

  • Unknown Types: When you do not need to know the actual type parameter, you may use star projections to handle the generic types without explicitly specifying the type that saves boiler plate code most of the time.
  • General-purpose APIs: Star projections enable the construction of APIs that will work with types whose parameters are not known or are not relevant to the operation being performed, which can make an API more versatile.

6. Safe Covariant Collections

The use of Kotlin’s out variance modifier enables collections to be safely covariant. So a List can be used wherever a List is expected provided the list is used only to read values.

  • Type-safe collections: The out modifier assures collections or data structures, which have been allocated for read-only purposes, are used without errors in a type-safe manner; hence, less error-prone and more flexible code is produced.

7. Better design by Consumer/Producer roles

Kotlin’s use of the variance system sees to it that the producer-consumer pattern is followed. This can easily reason about types that should behave in different contexts.

  • Producer roles: Producer Roles Covariant types with out are ideal usage when an object is a producer of values such as function or collection whose data you retrieve.
  • Consumer roles: Consumer Roles Contravariant types with in are ideal usage when an object is a consumer of values such as with callbacks or functions that accept other forms of data.

8. More Readable Code

Consequently, a type’s variance is declared explicitly in Kotlin that renders the above code much easier to read and understand. A person can easily determine whether some type is used together with a variable that has a contravariant or covariant position potentially leading to more maintainable codebases.

  • Explicit type intent: it uses in and out to clearly state which role a type parameter plays, so it’s much easier to reason about how the code works and what kind of types can be passed to or returned from functions.

9. Efficient Memory Usage

Using variance, you can limit which operations-inputs or outputs-are allowed on a type. This can help avoid misuse of objects and reduce object manipulation.

  • Improved Type Handling: With variance, you will constrain how types can be used. This can prevent accidental copying or misuse of objects in ways that lead to inefficiencies in memory.

Disadvantages of Variance In, Out, and Star Projections in Kotlin Language

Even though variance modifiers in, out of Kotlin offer many benefits to type safety and flexibility, they are certainly not free from some limitations and disadvantages; developers must be aware of these. Here is the main disadvantage of variance and star projections in Kotlin.

1. Increased Complexity in Type Definitions

The introduction of variance modifiers can add complexity to type definitions, making them harder to read and understand for developers who are not familiar with the concepts of covariance and contravariance.

  • Learning curve: New developers or those transitioning from languages without variance may find it challenging to grasp these concepts, leading to potential misuse or confusion.
  • Complicated generics: Complex generic types that employ multiple variance annotations can become cumbersome and difficult to decipher, impacting code readability.

2. Limited Use Cases for Star Projections

Star projections (*) offer a way to work with unknown types, but they come with restrictions that can limit their utility in some situations.

  • No type-specific operations: When using star projections, you cannot perform type-specific operations, such as invoking functions or accessing properties specific to the type. This limitation can lead to reduced functionality in certain scenarios.
  • Potential for runtime errors: Since star projections do not specify a concrete type, they may lead to runtime errors if not handled carefully, especially if assumptions are made about the type being used.

3. Overhead on performance

Using variance might make your program a little more expensive in terms of performance, but that very much depends on the situation and the amount of type checks and casts you really need.

  • Type checks at runtime: The use of variance requires runtime type checking. This may have a negative effect on the performance of the program, especially for programs where type safety is used very intensively in high-performance applications.
  • Further overhead: the use of generics with variance may lead to additional memory overhead due to the need for type-erased structures or wrappers, which, in turn may influence the overall efficiency of an application.

4. Misuse Potentiality

Although variance provides developers with a good tool to handle type relationships, the mechanism may also be misused, leading to unsafe code or even runtime exceptions.

  • Accidental contravariance/covariance: Developers may choose the wrong variance modifier for an unintended reason, which will silently create some sort of subtle bug-pesky bugs like passing the wrong type to a generic method declared to work with some variance.
  • Deep codebases: Managing variance over many layers of generics in a large codebase usually generates complex and potentially fragile code that tends to fail.

5. Not Always Necessary

Some instances may not require variance or overkill since they only add complexity with no obvious benefits.

  • Simple types: When one works with simple types or scenarios where the relationships between types are apparent, the introduction of variance only serves to make the design cumbersome and does not add any benefits to the system.
  • Alternative solutions: When you sometimes get away with taking a traditional approach; things become overly abstract when you introduce variance, and then you have hard-to-maintain code.

6. Java Interoperability Issues

Although the system of variance in Kotlin has been designed with excellent interoperability with Java, there are still far too many pitfalls when dealing with interfacing to Java code.

  • Wildcard limitations: Java’s use of wildcards sometimes causes confusion when using variability modifiers from the point of view of Kotlin, especially when expectations around type behavior are not properly aligned.
  • Type Erasure: In a mixed Kotlin-Java codebase, type erasure in Java poses problems when dealing with variance, and results in a pretty unpredictable pattern when variance is in use.

7. Tedious Type Declarations

By allowing us to declare variance, the type declarations turn out to be verbose, so the code is less clean and harder to maintain.

  • Long type signatures: If we use variance, then type signatures become longer and more complex, which generally makes for less clean code.
  • Needless specifications: The specification of variance sometimes is quite redundant or becomes a justification in itself, and that results in unnecessary boilerplate code.


Discover more from PiEmbSysTech

Subscribe to get the latest posts sent to your email.

Leave a Reply

Scroll to Top

Discover more from PiEmbSysTech

Subscribe now to keep reading and get access to the full archive.

Continue reading