Neko

Post

Understanding Variance in Java Covariant and Contravariant Types Explained

image

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

is a notation used in Java generics to specify a lower bound wildcard. It means that the type parameter A can be any type that is a supertype of B or B itself.

Here’s how it works:

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 reference.

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 to a reference of type List. This is to prevent potential type safety issues at runtime.

Full example you can find on Github.

This post is licensed under CC BY 4.0 by the author.