Учитывая, что 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, чтобы дать вам максимальное удовлетворение от принципа подстановки Лискова.