-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
207: Common Kotlin Interview Questions on Generics and Variance
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>>) {
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>>, Found: MediaSink<Media> (This part is fine)
pipeline(videoSource, mediaSink)
}
There are no comments for now.