You're reading for free via Android Dev Nexus' Friend Link. Become a member to access the best of Medium.

Member-only story

Kotlin Generics : Part 2 — Variance and Star Projections

Android Dev Nexus
Stackademic
Published in
7 min readSep 22, 2024

Hey Kotlin enthusiasts! 👋 Welcome back to our Kotlin Series, where we explore all the awesomeness Kotlin has to offer. In the first part 🔄 of our Kotlin Generics, we covered the basics — what Generics are, how they enable type safety and flexibility, and why they’re a powerful tool for writing reusable code. We covered the concepts like generic classes, functions, and type constraints, as well as the difference between Generics and Any. If you haven’t read it, below’s the link!

But there’s more to Generics. 💡 In this article, we’re going to level up by diving into advanced concepts of Kotlin Generics.

And hey, we’ve been holding a secret, all our articles are available for free. Here’s the link for this one. Enjoy (˵ ¬ᴗ¬˵)

Here’s what we’ll cover in Part 2:

🔍 What We’ll Explore:

  • Variance: Understanding how the in and out keywords help manage subtypes in generics.
  • Star Projections: Dealing with unknown types in generics.

Ready? Let’s get started! 💪

🔄 Variance: Understanding in and out

What’s Variance? 🤔

Lots of Cats :)

Let’s kick things off with a question: Can you use a list of cats where a list of animals is expected? After all, cats are animals, so shouldn’t a list of cats also qualify as a list of animals? 🤔 What do you think? Take a moment to consider it. 💭

Surprisingly, the answer is NO. But why? It’s because List is a generic or parametrically polymorphic type, where its type parameter is invariant.
So Variance answers a key question: How do generic types behave when dealing with inheritance?

Generics are invariant by default, meaning an instance of a generic class with a specific type cannot be implicitly converted to instances with its subtypes or supertypes.

CoVariant (out) – When You Only "Produce" 🏭

Let’s explore Covariance using a simple example of a Bag of fruits. Covariance allows us to use a more specific type (like Apple) where a more general type (like Fruit) is expected, but only when we are "producing" or reading from the generic type.

So Covariance lets us use a subclass (Apple) where a superclass (Fruit) is expected. In Kotlin, we use out keyword to define a structure that needs itself and its subclasses.

open class Fruit
class Apple : Fruit()

// Bag is a covariant class that can only produce (read) items of type T
class Bag<out T>(private val fruits: List<T>) {
fun getFruits(): List<T> = fruits
}

fun displayFruits(bag: Bag<Fruit>) {
val fruits = bag.getFruits()
for (fruit in fruits) {
println(fruit)
}
}

fun main() {
val appleBag = Bag(listOf(Apple(), Apple()))

// Covariance allows us to pass Bag<Apple> and Bag<Orange> as Bag<Fruit>
displayFruits(appleBag) // Works because Apple is a Fruit
}

Bag<out T> means the bag can hold items of type T or any subtype of T, but we can only read items from it (hence, it’s covariant).

ContraVariant (in) – When you Only "Consume" 🍽️

Let’s talk about making juice — because who doesn’t love fresh juice, right? 🍊 Now, imagine you’ve got a juicer that can handle different types of fruit. Instead of having separate juicers for apples, oranges, or bananas, wouldn’t it be awesome to have one juicer that can handle all kinds of fruits? That’s where contravariance comes into play, allowing us to accept superclasses (like Fruit) when the juicer is expected to handle specific fruits (like Apple or Orange).

We use the in keyword to mark types that will only be consumed (i.e., passed in) by the function or class.

// Base class
open class Fruit(val name: String)

// Subclasses
class Orange : Fruit("Orange")

// Contravariant Juicer
class Juicer<in T : Fruit> {
fun juice(fruit: T) {
println("Juicing ${fruit.name}!")
}
}

fun main() {
val fruitJuicer: Juicer<Fruit> = Juicer()

// Juicer<Fruit> can juice any type of Fruit, including Orange
fruitJuicer.juice(Orange()) // Juices an orange 🍊
}

We’ve got a Juicer<in T> where T is contravariant. This allows us to use a Juicer<Fruit> when a Juicer<Orange> is expected. So, the juicer can handle any type of fruit!

From Kotlin’s Official Documentation:
The key to understanding why this works is rather simple: if you can only take items from a collection, then using a collection of Strings and reading Objects from it is fine. Conversely, if you can only put items into the collection, it's okay to take a collection of Objects and put Strings into it ( )

🌟 Star Projections: Handling Unknown Types

Ever run into situations where you don’t know exactly what type a generic class or function is working with? Enter star projections (*), a handy way to deal with unknown types while still keeping your code type-safe.

Star Projections in Action ✨

Let’s start with an example code. What’s wrong in the below code?

// Generic Cage class
class Cage<T : Animal>(val animal: T) {
fun getAnimal(): T = animal
}

// Base Animal class
open class Animal(val name: String) {
fun makeSound() {
println("$name makes a sound.")
}
}

// Subclasses
class Dog(name: String) : Animal(name)
class Cat(name: String) : Animal(name)

fun main(){
val dog = Cage(Dog("Buddy"))
val cat = Cage(Cat("Whiskers"))

println(check(cat))
}

fun check(a: Any) : Boolean = a is Cage<Cat>

Due to type erasure (which we’ll cover in next post), we cannot verify if Cage was instantiated with the Cat type. When we attempt to check whether the generic type is the specific object it was instantiated with, we encounter a compilation error.

Therefore, we cannot check here what type it is? Then how to correct this? We use star projections denoted by * . Star projections (*) allow us to work with a generic type, even when the specific type parameter is unknown.

fun check(a: Any) : Boolean = a is Cage<*>
// We don't care about the specific type, we just know its a animal.

Cage<*>: This means we don't care about the exact type parameter. We just know that the Cage holds some kind of Animal, so we can safely call methods from the Animal class. Note here that we can only interact with methods and properties that belong to the upper bound of the generic type, for example Animal class here.

fun printUnknownList(list: List<*>) {
for (item in list) {
println(item) // Safe to print, but we don't know the exact type
}
}

fun addInList(list: MutableList<*>) {
list.add("anotherElement") // Error: cannot add elements to a list that is defined with a wildcard type (List<*>)
}

val mixedList = mutableListOf("Kotlin", 42, true)
printUnknownList(mixedList) // Prints each item, no matter the type!

addInList function. throws an error because wildcards represent an unknown type, and Kotlin does not allow adding items to such lists because it cannot guarantee type safety.

From Kotlin’s Official Documentation:

For Foo<out T : TUpper>, where T is a covariant type parameter with the upper bound TUpper, Foo<*> is equivalent to Foo<out TUpper>. This means that when the T is unknown you can safely read values of TUpper from Foo<*>.

For Foo<in T>, where T is a contravariant type parameter, Foo<*> is equivalent to Foo<in Nothing>. This means there is nothing you can write to Foo<*> in a safe way when T is unknown.

In our third and final article, we will explore additional concepts such as Type Erasure and Reified Types, along with other related topics.

If you found this helpful, don’t forget to clap for the blog or buy us a coffee here ☕️! Follow for more such posts.
Until then, happy coding! ٩(^ᗜ^ )و

References:
Kotlin and Android Official Documentation
Covariance and Contravariance by Christopher Okhravi
The Ins and Outs of Generic Variance in Kotlin: https://typealias.com/guides/ins-and-outs-of-generic-variance/

Stackademic 🎓

Thank you for reading until the end. Before you go:

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Written by Android Dev Nexus

Your friendly neighborhood developers working at PayPal, turning Android fundamentals into concepts you’ll actually understand.

Responses (3)

Awesome post! Looking forward to the next article on Reified🤩🤩

I had so much trouble using it ..but this blog helped.
Thank you 😭

Wow. Great post on Generics.