Перейти к основному содержимому

Ковариантный возвращаемый тип в Java

· 3 мин. чтения

1. Обзор

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

2. Ковариация

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

Рассмотрим пару основных примеров ковариации:

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

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

3. Ковариантный тип возврата

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

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

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

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

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() , возможность ковариантных возвращаемых типов позволяет нам иметь более конкретный возвращаемый объект, чем сам объект .

5. Вывод

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

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