«1. Обзор

В этом руководстве мы более подробно рассмотрим ковариантный возвращаемый тип в Java. Прежде чем рассматривать ковариацию с точки зрения типа возвращаемого значения, давайте посмотрим, что это значит.

2. Ковариантность

Ковариантность можно рассматривать как соглашение о том, как принимается подтип, когда определен только супертип.

Давайте рассмотрим несколько основных примеров ковариантности:

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

Итак, ковариантность означает, что мы можем получить доступ к определенным элементам, определенным через их супертип. Однако нам не разрешено помещать элементы в ковариантную систему, поскольку компилятор не сможет определить фактический тип универсальной структуры.

3. Ковариантный тип возвращаемого значения

Ковариантный тип возвращаемого значения — когда мы переопределяем метод — позволяет возвращаемому типу быть подтипом типа переопределенного метода.

Чтобы применить это на практике, давайте возьмем простой класс Producer с методом product(). По умолчанию он возвращает String как объект, чтобы обеспечить гибкость для дочерних классов:

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

Поскольку объект является типом возвращаемого значения, мы можем иметь более конкретный тип возвращаемого значения в дочернем классе. Это будет ковариантный возвращаемый тип, который будет производить числа из последовательностей символов:

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

4. Использование структуры

Основная идея ковариантных возвращаемых типов — поддержка замены Лискова.

Например, давайте рассмотрим следующий сценарий производителя:

@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());
}

После изменения на IntegerProducer бизнес-логика, которая фактически производит результат, может остаться прежней:

@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);
}

Однако мы все еще ссылаемся результат через объект. Всякий раз, когда мы начинаем использовать явную ссылку на IntegerProducer, мы можем получить результат как целое число без понижающего приведения:

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

    Integer result = producer.produce(integerAsString);

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

Известным сценарием является метод Object#clone, который по умолчанию возвращает объект. Всякий раз, когда мы переопределяем метод clone(), возможность ковариантных возвращаемых типов позволяет нам иметь более конкретный возвращаемый объект, чем сам объект.

5. Заключение

В этой статье мы увидели, что такое ковариантные и ковариантные возвращаемые типы и как они ведут себя в Java.

Как всегда, код доступен на GitHub.