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

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
andout
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? 🤔

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 ofString
s and readingObject
s from it is fine. Conversely, if you can only put items into the collection, it's okay to take a collection ofObject
s and putString
s 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>
, whereT
is a covariant type parameter with the upper boundTUpper
,Foo<*>
is equivalent toFoo<out TUpper>
. This means that when theT
is unknown you can safely read values ofTUpper
fromFoo<*>
.For
Foo<in T>
, whereT
is a contravariant type parameter,Foo<*>
is equivalent toFoo<in Nothing>
. This means there is nothing you can write toFoo<*>
in a safe way whenT
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:
- Please consider clapping and following the writer! 👏
- Follow us X | LinkedIn | YouTube | Discord | Newsletter | Podcast
- Create a free AI-powered blog on Differ.
- More content at Stackademic.com