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.
Recent Comments