Учитывая, что Cat is
a подкласс Animal,
должен List<Cat>
также быть подклассом List<Animal>
?
Что вы думаете? Должен ли List<Cat>
быть List<Animal>
? Что такое Function<? super T, ? extends U>
в Java? Понимание концепции дисперсии - ключ к ответу на эти два, казалось бы, разных вопроса.
Ковариация
Разве не было бы замечательно, если бы везде, где принимается List<Animal>
, можно было бы сдать и List<Cat>
? Но возникнут ли проблемы, если компилятор позволит это?
Давайте исследуем это, сначала определив тип List<T>
следующим образом, чтобы наше обсуждение оставалось простым:
Теперь давайте создадим функцию foo
, которая принимает в качестве входных данных List<Animal>
. В теле функции мы получаем первый элемент из списка и вызываем некоторые из его методов:
Посмотрим, что произойдет, если мы попытаемся передать List<Cat>
, где List<Animal>
принимается:
Строка 2 foo
в листинге 2 обращается к первому элементу входного списка, который оказывается Cat
. Но это не проблема, потому что foo
обращается только к таким методам, как getName()
(в строке 3), которые определены в Animal.
. Любой экземпляр Cat
также должен иметь этот метод, поскольку Cat
является подтипом Animal
. Так что получается.
Фактически, мы говорим, что параметр типа T
является ковариантным в универсальном типе M<T>
, если оба следующих условия верны:
A
- это подтипB
.M<A>
- это подтипM<B>
.
В таких языках, как Scala или C #, вы можете пометить параметры типа в универсальном классе как ковариантные с помощью специального символа или ключевого слова:
Но в Java нет эквивалента этих маркеров. Фактически, с List<T>
, который мы определили ранее, Java не позволяет передавать List<Cat>
в List<Animal>
.
Так как же достичь ковариации в Java? Нет способа сделать это в определении List<T>
. Оказывается, вы можете обеспечить ковариацию, когда используется тип списка. Вернемся к foo
и перепишем его так:
Обратите внимание на использование подстановочного знака (вопросительного знака). Теперь мы можем передать List<Cat>
или List
любого подтипа Animal
.
Контравариантность
Вы можете спросить, существует ли обратный случай, когда, если A
является подтипом B
, M<B>
является подтипом M<A>
. Там есть! И это называется контравариантностью.
Рассмотрим тип компаратора:
И функция bar
, которая принимает Comparator<Cat>
в качестве входных данных:
Теперь попробуем передать Comparator<Animal>
bar
:
Это вызовет проблемы? Нет. Строка 4 листинга 7 передает Cat
экземпляров в isGreaterThanOrEqualTo
, и это нормально, потому что метод ожидает Animal
экземпляров.
Фактически, мы говорим, что параметр типа T
является контравариантным в универсальном типе M<T>
, если оба утверждения верны:
A
- это подтипB
.M<B>
- это подтипM<A>
.
Точно так же в Java, если нам нужно разрешить контравариантность, нам нужно объявить, что где используется Comparator
:
В чем на самом деле разница?
Обратите внимание, что T
в List<T>
появляется только как возвращаемое значение (см. T get(int index)
). Также обратите внимание, что T
в Comparator<T>
появляется только как вход для функции (см. boolean isGreaterThanOrEqualTo(T t1, T t2)
).
Итак, как правило, если ваш параметр типа появляется только как возвращаемое значение в методах универсального типа, он, скорее всего, ковариантен. Точно так же, если он появляется только как входной тип методов, он, скорее всего, контравариантен.
Интересный случай функционального интерфейса
Теперь вернемся к примеру интерфейса Function
, приведенному в начале этой статьи. Вот еще контекст:
Почему T
определяется как контравариантный, а U
как ковариантный? Посмотрим на интерфейс Function
:
T
появляется только как тип ввода, а U
только как тип вывода, что делает T
ковариантным и U
вариантом. Это означает, что, учитывая, что Cat
является подтипом Animal
, а Car
является подтипом Vehicle
, у нас будет Function<Animal, Car>
подтип Function<Cat, Vehicle>
. То есть, когда ожидается Function<Cat, Vehicle>
, вы можете передать ему Function<Animal, Car>
.
Заключение
В следующий раз, когда вы увидите ? extends T
или ? super U
, не удивляйтесь, поскольку автор кода просто пытается обойти ограничение системы типов Java, чтобы дать вам максимальное удовлетворение от принципа подстановки Лискова.