Kotlin's `in` and `out`: Declaration-Site Variance

Kotlin's out and in keywords define a generic class as a producer or consumer at its declaration, avoiding Java's repetitive wildcards. Use out T for types only returned (produced) and in T for types only consumed.
Why it exists
By default, generic types are invariant. This means a List<String> is not a subtype of List<Object>. This rule prevents you from accidentally adding an Integer to a list of strings. However, this strictness makes safe, common operations difficult, like passing a list of strings to a function that processes a list of objects. Java solves this with use-site wildcards (? extends, ? super), but Kotlin offers a cleaner, declaration-site solution.
The mental model
Think of variance as defining a generic class's contract. Is this class a producer that only gives you items of type T, or a consumer that only accepts items of type T? Kotlin's out and in keywords bake this producer/consumer role directly into the class declaration, making the code that uses the class simpler and more readable.
How it works
Kotlin lets you specify variance on the class or interface itself.
out T (Covariance): Marks T as a produced type. The class can only use T in 'out' positions, like function return values. This allows a Source<String> to be safely used where a Source<Object> is expected. This is analogous to Java's ? extends T.
in T (Contravariance): Marks T as a consumed type. The class can only use T in 'in' positions, like function arguments. This allows a Comparer<Object> to be used where a Comparer<String> is expected, because a function that can compare any two objects can certainly compare two strings. This is analogous to Java's ? super T.
If a generic type is used in both 'in' and 'out' positions, it must remain invariant (no keyword).
When to use it
Use declaration-site variance when designing generic classes with a clear, one-way flow for the generic type. A Factory<out T> that only produces objects or a Logger<in T> that only consumes objects are perfect candidates. This makes your API more flexible for its users.
When not to use it
Do not use variance modifiers on a generic class that both accepts and returns its generic type in its public methods. A prime example is a mutable collection like MutableList<T>, which has both an add(element: T) method (consumer) and a get(index: Int): T method (producer). The Kotlin compiler will prevent you from declaring such a class with in or out.
One canonical example
The standard library's List<out E> interface is covariant. Because E is an out type, you can assign a List<String> to a variable of type List<Any>. This is safe because List only has producer methods for E, like get(). In contrast, MutableList<E> is invariant. It has consumer methods like add(), so you cannot assign a MutableList<String> to a MutableList<Any> and risk adding a non-String element.
Interview question
What is the primary reason Kotlin's MutableList<E> is invariant, while List<out E> is covariant?
- a.MutableList is a mutable data structure, and mutability inherently prevents the application of variance modifiers for type safety.
- b.MutableList allows elements of type E to be both added to and retrieved from the list, requiring E to be used in both 'in' and 'out' positions.Correct
- c.MutableList needs to support Java's ? super T wildcards, which conflicts with Kotlin's declaration-site out variance.
- d.The MutableList interface is designed to prevent accidental runtime type casting errors, which declaration-site variance cannot guarantee.
Why? this is the answer
The card explains that a generic type must remain invariant if it is used in both 'in' (consumer, like add) and 'out' (producer, like get) positions, which is precisely the case for MutableList. Option A is tempting because mutability is the context, but the direct reason for invariance is the dual usage of the generic type in 'in' and 'out' positions, as per Kotlin's variance rules.
Just read this? Test yourself on what you have been reading.
Read the original → kotlinlang.org
You just looked this up. Could you explain it out loud?
That is the part interviews actually test. Tezvyn takes questions like this one and gives you what the interviewer is really checking, the answer that lands, and the mistake that ends the conversation, in the four minutes before your next meeting.
The iPhone app is on the way
We are building it. Until it lands, nothing here is held back from you: every interview card, your saved cards, streaks and the job board all work in Safari, plus hundreds of free practice quizzes of thirty questions each. Sign in and it all carries over to the app the day it arrives.
Want it as an icon? Tap Share at the bottom of Safari, then Add to Home Screen. It opens full screen and the cards you have read stay available offline.
We are hiring for this. Open roles that interview on kotlin — each one lists the topics its interview covers.
See open roles