Understanding Variance in Java Covariant and Contravariant Types Explained
In Java, covariant and contravariant types are concepts related to subtyping and inheritance. They deal with the relationship between types when dealing with inheritance hierarchies and method overriding.
Covariant Types
Covariant types relate to the return types of methods in subclasses. In a covariant relationship, the return type of a method in a subclass can be a subtype of the return type of the same method in the superclass. This allows for more specific types to be returned in subclasses, enabling a more flexible and intuitive use of polymorphism.
For example, consider a class hierarchy where Animal is the superclass and Dog is a subclass. If the Animal class has a method makeSound() that returns an AnimalSound, the Dog class can override this method to return a more specific type, such as DogSound, as long as DogSound is a subtype of AnimalSound.
Contravariant Types
Contravariant types, on the other hand, relate to the parameters of methods in subclasses. In a contravariant relationship, the parameter types of a method in a subclass can be supertypes of the parameter types of the same method in the superclass. This allows for more general types to be accepted as parameters in subclasses, again promoting flexibility and polymorphism.
Continuing with the example above, if the Animal class has a method feed(Food food) that accepts any type of Food, the Dog class can override this method to accept a more specific type of Food, such as DogFood, as long as DogFood is a supertype of Food.
Java Syntax
In Java, covariant types are naturally supported in method overriding. When overriding a method in a subclass, the return type of the overridden method can be a subtype of the return type in the superclass.
Contravariant types are not directly supported in Java, as method parameters must match exactly in type and not be supertypes. However, you can achieve contravariant behavior by using wildcard types in method parameters, allowing more general types to be accepted.
Understanding covariant and contravariant types is crucial for writing flexible and maintainable Java code, as it enables better utilization of inheritance and polymorphism features provided by the language.
Java covariant types
Covariant types can also be illustrated with collections in Java. Let’s consider a scenario where we have a hierarchy of classes representing animals, with Animal as the superclass and Dog as a subclass. We’ll demonstrate covariant types by using a collection with the superclass type and adding objects of the subclass to it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.ArrayList;
import java.util.List;
class Animal {
public String getType() {
return "Animal";
}
}
class Dog extends Animal {
@Override
public String getType() {
return "Dog";
}
}
public class Main {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Dog());
// Output: Dog
for (Animal animal : animals) {
System.out.println(animal.getType());
}
}
}
Java Contravariant types
Contravariant types are not directly supported in Java for arrays or collections, as Java arrays and generics are invariant. However, we can illustrate a scenario where contravariant behavior can be simulated using wildcard types in collections.
Consider a scenario where we have a hierarchy of classes representing animals, with Animal as the superclass and Dog as a subclass. We’ll demonstrate a contravariant-like behavior by using a collection with the subclass type as a parameter to a method that accepts a collection with the superclass type.
Although Java arrays and collections do not directly support contravariance, we can achieve a similar effect using wildcard types in method parameters.
By accepting a collection with a wildcard type (List<? extends Animal>), the method can process collections containing objects of Animal or its subtypes, allowing for a contravariant-like behavior.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.ArrayList;
import java.util.List;
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
// Method that accepts a collection of Animal
public static void processAnimals(List<? extends Animal> animals) {
for (Animal animal : animals) {
animal.makeSound();
}
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
// Pass a List<Dog> to a method that accepts List<? extends Animal>
processAnimals(dogs);
}
}
Java <? super B> contravariant syntax
Here’s how it works:
- A can be any type that is a supertype of B. This includes B itself, as well as any class or interface that B extends or implements, and any superclass of B.
- Using allows you to work with objects of type A or any of its supertypes up to B. This is useful when you want to accept a range of types that are broader than B.
- You can use <? super B> as a wildcard in method parameters or as a type argument in generic classes or methods.
Here’s an example to illustrate :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.ArrayList;
import java.util.List;
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
List<? super Dog> animals = new ArrayList<Animal>(); // List of a supertype of Dog
animals.add(new Dog()); // This is valid
//animals.add(new Animal()); // This is not valid
// Dog d = animals.get(0); // Compilation error, can't guarantee the exact type
Object o = animals.get(0); // Valid, since any supertype of Dog can be assigned to Object
}
}
In this example:
- We create a list animals with a wildcard type <? super Dog>. This means the list can hold objects of type Dog or any supertype of Dog.
- We add a Dog object and an Animal object to the list. Since both Dog and Animal are supertypes of Dog, this is valid.
- When retrieving elements from the list, the compiler can only guarantee that the elements are of a type that is a supertype of Dog, so the type of the retrieved object is Object.
Java Invariant types
Invariance means that the type relationship remains unchanged between the type parameter and its subtypes or supertypes. In Java, arrays and generics are invariant, meaning that if you have a type A and a subtype B, you cannot directly use an array or collection of B where A is expected.
Here’s an example to illustrate invariance with arrays:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
Animal[] animals = new Dog[1]; // Compiler error
animals[0] = new Animal(); // This would cause a runtime error
}
}
This results in a compiler error because arrays in Java are invariant. Even though Dog is a subtype of Animal, you cannot assign an array of Dog to a reference of type Animal[]. This prevents potential type safety issues at runtime.
Similarly, invariance is observed with generics:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.ArrayList;
import java.util.List;
class Animal {
public void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("Dog barks");
}
}
public class Main {
public static void main(String[] args) {
List<Animal> animals = new ArrayList<Dog>(); // Compiler error
animals.add(new Animal()); // This would cause a runtime error
}
}
In this example:
We attempt to create an ArrayList of Dog objects and assign it to a List
This results in a compiler error because generics in Java are also invariant. Even though Dog is a subtype of Animal, you cannot assign a List
Full example you can find on Github.