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