Generics & Variance Quiz
KOTLIN › Types & Classes
An interface is declared interface Producer<out T> { fun produce(): T }. Which assignment compiles?
- val p: Producer<Number> = anyProducer, a Producer<Any>
- val p: Producer<Any> = stringProducer, a Producer<String>
- Adding fun consume(item: T) compiles fine next to produce()
- Producer<String> and Producer<Any> are entirely unrelated
Answer: val p: Producer<Any> = stringProducer, a Producer<String>
out makes T covariant, so Producer<String> is a subtype of Producer<Any> and the assignment is valid. A consume(item: T) method would put T in an in-position and fail to compile.
Why can Comparable<in T> accept val y: Comparable<Double> = x when x is Comparable<Number>?
- Because Double is incorrectly treated as a supertype of Number here
- Because Comparable<T> is actually covariant in its type parameter T
- Because a star projection silently erases the Number/Double distinction
- in gives contravariance, so Comparable<Number> <: Comparable<Double>
Answer: in gives contravariance, so Comparable<Number> <: Comparable<Double>
in declares contravariance: a consumer of Number can consume any Double, so Comparable<Number> is a subtype of Comparable<Double>, making the assignment safe.
What does the type projection Array<out Any> restrict you from doing?
- Calling set() / writing elements, because it’s producer-only
- Calling get() / reading elements, since reads are disallowed
- Passing an Array<Int> as the argument, because out forbids it
- Reading elements as type Any, because out blocks all access
Answer: Calling set() / writing elements, because it’s producer-only
Projecting Array with out makes it a producer; only out-position members like get() are allowed, so set() (which consumes T) is forbidden. This lets you pass Array<Int> safely.
Which runtime check is allowed under Kotlin's type erasure?
- value is List<Int>
- value is Map<String, Int>
- value is List<*>
- value is T inside a regular (non-inline) generic function
Answer: value is List<*>
Type arguments are erased, so you can only check the star-projected form is List<*>. Checks against concrete type arguments, or is T in a non-reified context, are rejected by the compiler.
Why can a reified type parameter only appear in an inline function?
- Because reified turns off type erasure for the entire program, so every generic keeps its real type
- Because inline functions execute in a special runtime that provides reflection and keeps type details
- Because reified parameters are always passed as a Class<T> value behind the scenes automatically
- Because inlining copies the body to each call site, where the concrete type is known and substituted
Answer: Because inlining copies the body to each call site, where the concrete type is known and substituted
Inlining substitutes the actual type argument into the generated code at each call site, so T::class and is T resolve to a concrete type. A non-inline function has no call-site body to specialize, so the type stays erased.
Given class Box<T>, which statement about variance is correct?
- Box<T> is covariant by default since T is left unconstrained
- Box<Dog> is automatically a subtype of Box<Animal> here
- Box<T> is invariant; Box<Dog> and Box<Animal> are unrelated
- Box<*> means Box<Any>, permitting both reads and writes of Any
Answer: Box<T> is invariant; Box<Dog> and Box<Animal> are unrelated
Without an in/out modifier a generic class is invariant, so Box<Dog> and Box<Animal> are unrelated. Variance must be added via declaration-site (in/out) or use-site projection; Box<*> reads as out Any? and writes as in Nothing.
You need a function whose type parameter T is simultaneously a CharSequence and a Comparable<T>. Which declaration compiles in Kotlin?
- fun <T : CharSequence, Comparable<T>> f() with both bounds inline
- fun <T : CharSequence & Comparable<T>> f() using Java-style type bounds
- fun <T> f() where T : CharSequence, T : Comparable<T>
- fun <T : CharSequence : Comparable<T>> f() with two colons in one bound
Answer: fun <T> f() where T : CharSequence, T : Comparable<T>
A single inline bound only allows one type after the colon, so multiple upper bounds must be expressed with a where clause listing each constraint separately. The other forms use invalid syntax (the & intersection form is Java, not Kotlin).
Given interface Consumer<in T> { fun consume(t: T) }, what does the star projection Consumer<*> mean?
- Consumer<out Any?>, so you can safely read a produced T value
- Consumer<Any>, so consume() accepts any non-null value you pass
- It ignores variance and behaves like the raw type in Kotlin
- Consumer<in Nothing>, so no value can be passed to consume()
Answer: Consumer<in Nothing>, so no value can be passed to consume()
For a contravariant parameter, the star projection degrades to in Nothing because the only type guaranteed to be a subtype of every possible T is Nothing. That makes consume() effectively uncallable with a real argument through the projection.
When Kotlin code calls a Java method declared void addAll(Collection<? extends T> items), how does Kotlin interpret that wildcard?
- As an invariant Collection<T>
- As a covariant projection Collection<out T>
- As a contravariant projection Collection<in T>
- As a star projection Collection<*>
Answer: As a covariant projection Collection<out T>
Java's ? extends T upper-bounded wildcard maps directly onto Kotlin's use-site out projection, since both express covariance (producer of T). A ? super T lower-bounded wildcard would instead map to Collection<in T>.
In a covariant class Source<out T>, where is it still legal for T to appear in an in (consumer) position?
- On a private member, or with @UnsafeVariance on a public one
- Nowhere; the compiler bans T from every in-position in the class
- On any public function, as long as the parameter has a default value
- Only inside functions that are also declared inline and public
Answer: On a private member, or with @UnsafeVariance on a public one
Declaration-site variance is checked only against the externally visible API, so T may freely sit in an in-position on private members. The @UnsafeVariance annotation explicitly suppresses the check when you knowingly need T in a contravariant spot on a public member.
Which of these fails to compile specifically because of JVM type erasure?
- Declaring fun <T> wrap(t: T): Box<T> in a generic helper class
- Calling list.filterIsInstance<String>() on a list of mixed values
- Using vararg items: T inside a generic function declaration
- Declaring both fun f(p: List<String>) and fun f(p: List<Int>)
Answer: Declaring both fun f(p: List<String>) and fun f(p: List<Int>)
After erasure both overloads have the identical JVM signature f(List), producing a platform declaration clash. The other three are all valid because they do not depend on distinguishing erased generic arguments at runtime.
What capability does a reified T add inside an inline function that an ordinary type parameter lacks?
- Declaring T with two or more upper bounds in the signature
- Making T implicitly covariant at every call site in Kotlin
- Using T::class, x is T, and x as T for the real type
- Keeping T available unchanged across coroutine suspension
Answer: Using T::class, x is T, and x as T for the real type
Because the function body is inlined with the real type argument substituted in, reified lets you reference the concrete type at runtime via T::class, is T, and as T. Variance, multiple bounds, and suspension are unrelated to reification.