Covariance and Contravariance Explained
Covariance and contravariance explained with PECS, bounded wildcards and Java examples for interview prep.
Expected Interview Answer
Covariance and contravariance describe how subtyping relationships between types carry over to derived types like arrays, generics, and function signatures โ covariance preserves the subtype direction, contravariance reverses it, and invariance keeps none.
If Cat is a subtype of Animal, a covariant construct treats List<Cat> as a subtype of List<Animal>, which is only safe for read-only producers. A contravariant construct reverses the direction: a Comparator<Animal> can act as a Comparator<Cat>, because anything that can compare general Animals can certainly compare specific Cats โ this is safe for consumers. Function parameter types are typically contravariant while return types are covariant, which is why overriding methods may widen a parameter type in some languages but must narrow or match the return type. Java models this with bounded wildcards (`? extends T` for covariance, `? super T` for contravariance, summarized by the PECS rule: Producer Extends, Consumer Super) rather than making generics variant by default, precisely to avoid the runtime `ArrayStoreException` class of bugs that plain covariant arrays introduced.
- Prevents unsafe writes into a covariantly-typed read structure
- Lets consumer APIs like Comparator accept broader supertypes safely
- Explains why overriding return types can narrow (covariant returns)
- Underlies correct use of Java bounded wildcards and Kotlin/C# variance annotations
AI Mentor Explanation
A scouting report writer who can evaluate any Batter can certainly evaluate a specific Opener, so a ReportWriter<Batter> safely substitutes for a ReportWriter<Opener> role โ that is contravariance, the consumer direction widens. In reverse, a read-only roster of Openers can be handed anywhere a read-only roster of Batters is expected, since every Opener is already a Batter โ that is covariance, safe only because nobody inserts a generic Batter back into the Opener-only list. Mixing the two without care is how a bowling-only sub-list ends up promised a batting insertion it cannot type-check.
Step-by-Step Explanation
Step 1
Establish the subtype relation
Confirm Cat is a subtype of Animal before reasoning about variance.
Step 2
Identify producer vs consumer position
A type used only to read/produce values can be covariant; one used only to accept/consume values can be contravariant.
Step 3
Apply PECS
Producer Extends, Consumer Super โ use `? extends T` for read-only sources and `? super T` for write-only sinks.
Step 4
Reject unsafe mixed use
A type used for both reading and writing must stay invariant to remain type-safe.
What Interviewer Expects
- Correct definitions of covariance, contravariance, and invariance
- Explanation of why plain array covariance in Java is unsafe (ArrayStoreException)
- Correct application of PECS with bounded wildcards
- Awareness that method return types are covariant and parameter types are contravariant under overriding
Common Mistakes
- Confusing covariance with simple inheritance/polymorphism
- Believing Java generics are covariant by default like arrays
- Using `? extends T` for a type that also needs writes
- Forgetting contravariance applies to consumer/input positions, not producer/output positions
Best Answer (HR Friendly)
โCovariance and contravariance are rules about how substitutability works for generic or array types. Covariance means a more specific type can stand in wherever a general type is expected, but only for reading. Contravariance flips that for things that consume values, like a comparator built for a general type also working for a specific one. Getting this wrong is how you get subtle runtime type errors, so most languages restrict it with wildcards rather than allowing it everywhere.โ
Code Example
class Animal {}
class Cat extends Animal {}
class Shelter {
// Producer: only reading Cats out, so covariant (? extends)
static double totalWeight(List<? extends Animal> animals) {
double sum = 0;
for (Animal a : animals) sum += 1.0; // read-only use
return sum;
}
// Consumer: only writing Cats in, so contravariant (? super)
static void fillWithCats(List<? super Cat> sink, int count) {
for (int i = 0; i < count; i++) sink.add(new Cat());
}
}
List<Cat> cats = new ArrayList<>();
Shelter.totalWeight(cats); // List<Cat> accepted where ? extends Animal expected
List<Animal> animals = new ArrayList<>();
Shelter.fillWithCats(animals, 3); // List<Animal> accepted where ? super Cat expectedFollow-up Questions
- Why are Java arrays covariant but generics are not, by default?
- What is ArrayStoreException and how does it relate to covariance?
- How does C# express variance with `in` and `out` keywords on generic interfaces?
- Why must overriding methods have covariant return types but contravariant (or same) parameter types?
MCQ Practice
1. Which wildcard should you use for a generic parameter you only read from?
`? extends T` (covariant) is safe for producer/read-only positions.
2. Under the PECS rule, "Consumer Super" means you should use which wildcard for a write-only sink?
`? super T` (contravariant) is safe for consumer/write-only positions.
3. Why can Java arrays throw ArrayStoreException at runtime but generic lists cannot suffer the equivalent error?
Arrays keep runtime type info and are covariant, needing a runtime store check; generics rely on compile-time wildcard rules instead.
Flash Cards
Covariance in one line? โ Subtype direction is preserved โ List<Cat> usable as List<? extends Animal> for reading.
Contravariance in one line? โ Subtype direction is reversed โ Comparator<Animal> usable as Comparator<Cat> for consuming.
PECS stands for? โ Producer Extends, Consumer Super.
Why not make generics covariant by default? โ It would allow unsafe writes of a supertype into what is really a subtype-only container.