Java generics are a powerful feature that enable developers to write flexible, type-safe code. However, when it comes to subtyping and how generic types relate to each other, things can get a bit tricky. Three key concepts—covariance, invariance, and contravariance—help explain how generics behave in different scenarios. Let’s break down each one with clear explanations and examples.


Invariance: The Default Behavior

In Java, generics are invariant by default. This means that even if one type is a subtype of another, their corresponding generic types are not related.

Example:

List<Number> numbers = new ArrayList<Integer>(); // Compilation error!

Even though Integer is a subtype of Number, List<Integer> is not a subtype of List<Number>. This strictness ensures type safety, preventing accidental misuse of collections.


Covariance: Flexibility with Reading

Covariance allows a generic type to be a subtype if its type parameter is a subtype. In Java, you express covariance with the wildcard ? extends Type.

Example:

List<? extends Number> numbers = new ArrayList<Integer>();

Here, numbers can point to a List<Integer>, List<Double>, or any other list whose elements extend Number. However, you cannot add elements to numbers (except null) because the actual list could be of any subtype of Number. You can read elements as Number.

Use covariance when you only need to read from a structure, not write to it.


Contravariance: Flexibility with Writing

Contravariance is the opposite of covariance. It allows a generic type to be a supertype if its type parameter is a supertype. In Java, you use ? super Type for contravariance.

Example:

List<? super Integer> integers = new ArrayList<Number>();
integers.add(1); // OK
Object obj = integers.get(0); // Allowed
Integer num = integers.get(0); // Compilation error!

Here, integers can point to a List<Integer>, List<Number>, or even a List<Object>. You can add Integer values, but when you retrieve them, you only know they are Object.

Use contravariance when you need to write to a structure, but not read specific types from it.


Summary Table

Variance Syntax Can Read Can Write Example
Invariant List<T> Yes Yes List<Integer>
Covariant List<? extends T> Yes No List<? extends Number>
Contravariant List<? super T> No* Yes List<? super Integer>

*You can only read as Object.


Conclusion

Understanding covariance, invariance, and contravariance is essential for mastering Java generics. Remember:

  • Invariant: Exact type matches only.
  • Covariant (? extends T): Flexible for reading.
  • Contravariant (? super T): Flexible for writing.

By choosing the right variance for your use case, you can write safer and more expressive generic code in Java.