1. Overview

In this tutorial, we’re going to have a closer look at the covariant return type in Java. Before examining covariance from the return type’s point of view, let’s see what that means.

2. Covariance

Covariance can be considered as a contract for how a subtype is accepted when only the supertype is defined.

Let’s consider a couple of basic examples of covariance:

List<? extends Number> integerList = new ArrayList<Integer>();
List<? extends Number> doubleList = new ArrayList<Double>();

So covariance means, we can access specific elements defined via their supertype. However, we aren’t allowed to put elements into a covariant system, since the compiler would fail to determine the actual type of the generic structure.

3. The Covariant Return Type

The covariant return type is – when we override a method – what allows the return type to be the subtype of the type of the overridden method.

To put this into practice, let’s take a simple Producer class with a produce() method. By default, it returns a String as an Object to provide flexibility for the child classes:

public class Producer {
    public Object produce(String input) {
        Object result = input.toLowerCase();
        return result;
    }
}

As a result of the Object as a return type, we can have a more concrete return type in the child class. That will be the covariant return type and will produce numbers from character sequences:

public class IntegerProducer extends Producer {
    @Override
    public Integer produce(String input) {
        return Integer.parseInt(input);
    }
}

4. The Usage of the Structure

The main idea behind the covariant return types is to support the Liskov substitution.

For instance, let’s consider the following producer scenario:

@Test
public void whenInputIsArbitrary_thenProducerProducesString() {
    String arbitraryInput = "just a random text";
    Producer producer = new Producer();

    Object objectOutput = producer.produce(arbitraryInput);

    assertEquals(arbitraryInput, objectOutput);
    assertEquals(String.class, objectOutput.getClass());
}

After changing to IntegerProducer, the business logic which actually produces the result can remain the same:

@Test
public void whenInputIsSupported_thenProducerCreatesInteger() {
    String integerAsString = "42";
    Producer producer = new IntegerProducer();

    Object result = producer.produce(integerAsString);

    assertEquals(Integer.class, result.getClass());
    assertEquals(Integer.parseInt(integerAsString), result);
}

However, we’re still referencing the result via an Object. Whenever we start to use an explicit reference to the IntegerProducer, we can retrieve the result as an Integer without downcasting:

@Test
public void whenInputIsSupported_thenIntegerProducerCreatesIntegerWithoutCasting() {
    String integerAsString = "42";
    IntegerProducer producer = new IntegerProducer();

    Integer result = producer.produce(integerAsString);

    assertEquals(Integer.parseInt(integerAsString), result);
}

A well-known scenario is the Object#clone method, which returns an Object by default. Whenever we override the clone() method, the facility of covariant return types allows us to have a more concrete return object than the Object itself.

5. Conclusion

In this article, we saw what the covariance and covariant return types are and how they behave in Java.

As always, the code is available over on GitHub.

Course – LS (cat=Java)

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.