1. What is Covariance and Contravariance in Java?
1.1 Covariance in Java
Covariance allows us to assign a more derived type to a less derived type. In simpler terms, it refers to the ability of a generic type to preserve the assignment compatibility when dealing with a subtype. This concept is commonly found in arrays and generic types in Java.
Example:
class Animal {
void sound() { System.out.println("Animal sound"); }
}
class Dog extends Animal {
void sound() { System.out.println("Dog barks"); }
}
public class CovariantExample {
public static void main(String[] args) {
Animal[] animals = new Dog[10]; // Covariance in arrays
animals[0] = new Dog(); // Works because Dog is a subtype of Animal
for (Animal animal : animals) {
if (animal != null) {
animal.sound();
}
}
}
}
In this case, a Dog[] can be assigned to an Animal[], showcasing covariance in arrays.
1.2 Covariance with Generics (? extends)
With generics, covariance is applied using the ? extends T wildcard. This means that we can read from the generic structure, but we cannot modify it (other than assigning null). This limitation ensures type safety.
Example:
import java.util.ArrayList;
import java.util.List;
public class CovarianceGenerics {
public static void main(String[] args) {
List<? extends Animal> animals = new ArrayList<Dog>();
// Cannot add a Dog or an Animal because it is read-only
// animals.add(new Dog()); // Compilation error
// animals.add(new Animal()); // Compilation error
for (Animal animal : animals) {
animal.sound(); // This works fine because we are reading from the list
}
}
}
The ? extends wildcard ensures that we can access elements safely, knowing they will be at least of type Animal, but it prevents us from modifying the list since the exact subtype is unknown.
1.3 When to Use Covariance
Covariance is useful when you need to process a collection of objects but don’t need to modify them. If you are only reading elements, covariance ensures that all elements are a subtype of a given class while preserving type safety.
1.4 Demo Result
In the covariance example with arrays, the program successfully outputs Dog barks for each element in the animals[] array, even though the array is declared as Animal[]. This proves the flexibility of covariance when dealing with inheritance hierarchies.
2. Understanding Contravariance in Java
2.1 Contravariance with Generics (? super)
Contravariance is the opposite of covariance. With contravariance, you can assign a more general type to a reference that expects a more specific type. In Java, contravariance is implemented using the ? super T wildcard.
This allows you to modify the collection by adding elements, but reading from the collection is restricted to only objects of type Object (or a safe downcast).
Example:
import java.util.ArrayList;
import java.util.List;
public class ContravarianceExample {
public static void main(String[] args) {
List<? super Dog> animals = new ArrayList<Animal>(); // Contravariant type
animals.add(new Dog()); // Allowed because Dog is a subtype of Animal
// animals.add(new Animal()); // Compilation error: Only Dog or its subclasses allowed
Object obj = animals.get(0); // Can only retrieve as Object
}
}
Here, we can add Dog objects to the list, but we cannot add Animal objects directly. Contravariance ensures that the collection will only contain elements of type Dog or a subtype, preserving type safety while allowing modifications.
2.2 When to Use Contravariance
Contravariance is helpful when you need to modify a collection of objects, specifically when adding elements to a data structure. It ensures that the elements added are compatible with a specific type, providing flexibility in situations where you expect different subtypes to be added.
3. Covariance vs Contravariance: Key Differences
3.1 Covariance (? extends)
Covariance is used when you want to read from a collection. The collection is treated as a source, meaning you only need to ensure that the elements are subtypes of a specific type. However, it restricts you from modifying the collection.
3.2 Contravariance (? super)
Contravariance is used when you want to write to a collection. It allows you to add elements while enforcing that they are compatible with a specific type or any of its subtypes. However, it restricts reading the collection to ensure type safety.
3.3 How to Decide Between Covariance and Contravariance
To decide between covariance and contravariance, ask yourself: Do you need to read, write, or both?
- If you only need to read from the collection, use ? extends.
- If you need to write to the collection, use ? super.
- If you need both, consider using a more specific generic type or redesigning the structure.
4. Demo Results: What You Have Learned
By using ? extends, you allow flexibility in reading data from a collection while keeping the integrity of the types. The ? super wildcard allows flexibility when adding elements to a collection, ensuring that only the expected subtypes are added.
These two concepts, covariance and contravariance, are crucial when dealing with inheritance and generics in Java. Knowing when and how to apply them will enhance your ability to create robust, type-safe code.
5. Conclusion
Understanding covariance and contravariance in Java is essential for handling generics and wildcards effectively. These concepts offer flexibility in reading and writing data while ensuring type safety in inheritance hierarchies. By applying the techniques outlined in this article, you can write more adaptable and maintainable code.
If you have any questions or need further clarification on covariance and contravariance, feel free to leave a comment below!
Read posts more at : Understanding Covariance and Contravariance in Java
Top comments (0)