Skip to Content
Course content

207: Common Kotlin Interview Questions on Generics and Variance

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've sat on both sides of the interview table for a decade, and if there's one topic that makes candidates sweat more than algorithmic puzzles, it's Generics and Variance. Interviewers love it because it's the dividing line between someone who just "uses" Kotlin and someone who actually understands how the type system protects your code from crashing at runtime.

To wrap your head around this, imagine you have a set of specialized storage bins. If you have a bin specifically for Apples, you can tell anyone, "This is a bin of Fruit," and you'd be telling the truth. Anything you pull out of that bin is guaranteed to be a piece of fruit. That's the core of covariance. Now, imagine a Fruit Crusher. If a machine can crush any kind of fruit, it can definitely crush an apple. You can treat a "Fruit Crusher" as an "Apple Crusher" because it's designed to take in fruit. That's contravariance.

The 'out' Keyword: Producing Values

In Kotlin, covariance is declared with the out keyword. When you mark a type parameter as out T, you're telling the compiler: "This class only produces T; it never consumes it."


open class Animal
class Dog : Animal()

// This is covariant. We can only 'get' animals out of it.
interface Producer<out T> {
    fun produce(): T
}

fun main() {
    val dogProducer: Producer<Dog> = object : Producer<Dog> {
        override fun produce(): Dog = Dog()
    }
    
    // This works because of 'out'. A Producer of Dogs is a Producer of Animals.
    val animalProducer: Producer<Animal> = dogProducer 
}

If you tried to add a function like fun consume(item: T) to that Producer interface, the compiler would scream at you. Why? Because if you could pass an Animal into a Producer<Dog>, you might accidentally try to put a Cat into a Dog producer, and that's how you get runtime crashes. out ensures the type only flows out of the object.

The 'in' Keyword: Consuming Values

Contravariance is the mirror image, using the in keyword. This is for classes that consume T but never return it. This is common with listeners, comparators, or sinks.


interface Consumer<in T> {
    fun consume(item: T)
}

fun main() {
    val animalConsumer: Consumer<Animal> = object : Consumer<Animal> {
        override fun consume(item: Animal) = println("Processing animal")
    }
    
    // This works because of 'in'. A Consumer of Animals can handle Dogs.
    val dogConsumer: Consumer<Dog> = animalConsumer
}

I often see candidates get confused here. Just remember: in means the type flows into the object. If a function can handle any Animal, it is logically capable of handling a Dog. Therefore, Consumer<Animal> is a subtype of Consumer<Dog>.

The Runtime Secret: Type Erasure

A classic "gotcha" question is: "Does the JVM know that a List<String> is different from a List<Int> at runtime?" The answer is a hard no. This is called Type Erasure.

Because Kotlin targets the JVM, generic type information is stripped away during compilation. At runtime, a List<String> just looks like a List of Objects. This is why you can't do things like if (myList is List<String>). If you need to preserve type information at runtime, you have to use reified type parameters inside an inline function, which effectively copies the type check into the call site.

When to use Invariance

If you don't use in or out, your type is invariant. This is the default. You need this when your class both produces and consumes the type. Think of a MutableList. You can add items to it (consume) and get items from it (produce). If MutableList were covariant, you could cast a MutableList<Dog> to a MutableList<Animal> and then accidentally add a Cat to your list of dogs. The compiler prevents this by forcing the types to match exactly.




📋 Practical Task

Fixing the Variance Mismatch in a Media Pipeline

You are building a media processing pipeline. You have a MediaSource that provides data and a MediaSink that consumes data. However, the current implementation is invariant, causing a compiler error when trying to use a generic MediaSink for a specific VideoSink.

Your Task: Modify the MediaSource and MediaSink interfaces to use the correct variance keywords (in or out) so that the pipeline function compiles and runs correctly.


open class Media
class Video : Media()
class Audio : Media()

interface MediaSource<T> {
    fun nextFrame(): T
}

interface MediaSink<T> {
    fun save(frame: T)
}

fun pipeline(source: MediaSource<Media>, sink: MediaSink<Media&gt>) {
    sink.save(source.nextFrame())
}

fun main() {
    val videoSource = object : MediaSource<Video> {
        override fun nextFrame(): Video = Video()
    }
    
    val mediaSink = object : MediaSink<Media> {
        override fun save(frame: Media) = println("Saved media frame")
    }

    // ERROR: Type mismatch. 
    // Required: MediaSource<Media>, Found: MediaSource<Video>
    // Required: MediaSink<Media&gt>, Found: MediaSink<Media> (This part is fine)
    pipeline(videoSource, mediaSink) 
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.