Generics & Variance Quiz

KOTLIN › Types & Classes

An interface is declared interface Producer<out T> { fun produce(): T }. Which assignment compiles?

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

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?

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?

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?

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?

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?

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?

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?

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?

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?

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?

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.

Back to Generics & Variance