
В большинстве статей на тему вариантности авторы слишком быстро погружаются в детали и сложные схемы, из-за чего у людей которые только пытаются понять саму идею опускаются руки. Но в большинстве случаев для понимания деталей необходимо разобраться в самом принципе, после чего понимание деталей становиться тривиальной задачей. А понять принцип проще, если показать все на картинках и самых простых примерах. В этом и заключается цель данной статьи —это быс��рое понимание принципов инвариантности, ковариантности, контравариантности.
Самый простой вариант понять эти принципы на примере коллекций. Для примера используем пять классов которые последовательно наследуются друг от друга и коллекции ArrayList предназначенные для хранения экземпляров этих типов.

List<Animal> animalList = new ArrayList<>();
List<Mammal> mammalList = new ArrayList<>();
List<Predator> predatorList = new ArrayList<>();
List<Lion> lionList = new ArrayList<>();
List<AfricanLion> africanLionList = new ArrayList<>();Принцип инвариантности (Invariance).
Данный принцип подразумевает неизменность форм. В отношении обобщенных типов это говорит о жёсткой привязке требуемых данных к конкретным типам.
Примером этого служит жестко заданный тип данных хранящихся в коллекции.
List<Predator> predatorList;Для демонстрации используем список List с жёстко заданным типом данных, и попробуем присвоить ему ссылку на список с данными классов предков и наследников.
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<>();
List<Mammal> mammalList = new ArrayList<>();
List<Predator> predatorList = new ArrayList<>();
List<Lion> lionList = new ArrayList<>();
List<AfricanLion> africanLionList = new ArrayList<>();
List<Predator> predatorList1 = predatorList; // код будет правильно компилироваться и работать
List<Predator> predatorList2 = animalList; // ошибка компиляции "incompatible types"
List<Predator> predatorList3 = mammalList; // ошибка компиляции "incompatible types"
List<Predator> predatorList4 = lionList; // ошибка компиляции "incompatible types"
List<Predator> predatorList5 = africanLionList; // ошибка компиляции "incompatible types"
}Присвоить списку List<Predator> удалось только ссылку на коллекцию с данными типа Predator, не смотря на то что другие коллекции содержат данные типы которых являются наследниками или предками указанного типа. Соответственно, операция присваивания, является инвариантной к типу данных.
Второй пример демонстрирует принцип инвариантности аргумента метода doWork(list)
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<>();
List<Mammal> mammalList = new ArrayList<>();
List<Predator> predatorList = new ArrayList<>();
List<Lion> lionList = new ArrayList<>();
List<AfricanLion> africanLionList = new ArrayList<>();
doWork(animalList); // ошибка компиляции "incompatible types"
doWork(mammalList); // ошибка компиляции "incompatible types"
doWork(predatorList); // код будет правильно компилироваться и работать
doWork(lionList); // ошибка компиляции "incompatible types"
doWork(africanLionList); // ошибка компиляции "incompatible types"
}
private static void doWork(List<Predator> list) {
// work
}Как и в случае с присваиванием, список передаваемый методу doWork() должен содержать объекты типа Predator и никаких других вариантов.
На диаграмме инвариантность будет выглядеть так:

Принцип Ковариантности (Covariance)
Принцип ковариантности заключается в возможности использовать в качестве данных коллекции содержащие экземпляры объектов наследников или самого указанного класса.
В нашем примере указать, что аргумент является ковариантным, можно с использованием конструкции называемой wildcard и имеющей следующий вид : <? extends <тип>> .
doWork(List<? extends Predator> list);На русском языке эту конструкцию можно осмыслить как: "Неуказанный тип расширяющий класс Predator". Изменив код примера мы получим следующее:
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<>();
List<Mammal> mammalList = new ArrayList<>();
List<Predator> predatorList = new ArrayList<>();
List<Lion> lionList = new ArrayList<>();
List<AfricanLion> africanLionList = new ArrayList<>();
doWork(animalList); // ошибка компиляции "incompatible types"
doWork(mammalList); // ошибка компиляции "incompatible types"
doWork(predatorList); // код будет правильно компилироваться и работать
doWork(lionList); // код будет правильно компилироваться и работать
doWork(africanLionList);// код будет правильно компилироваться и работать
}
private static void doWork(List<? extends Predator> list) {
// work
}Следовательно в виде аргумента в метод doWork() могут быть переданы списки с типом данных Predator, Lion, AfricanLion т.е. указанный тип и наследники.
на диаграмме ковариантность обобщенных типов выглядит так:

Принцип контравариантности (Contravariance)
Принцип контравариантности прямо противоположен принципу ковариантности. Из чего следует, что относительно нашего примера в качестве данных можно использовать коллекции содержащие данные типов указанного класса или его предков.
Для обозначения контравариантного типа используется подобная ковариантной конструкция, но с ключевым словом super
doWork(List<? super Predator> list)Соответственно, это: Любой неуказанный тип являющийся предком класса Predator.
И наш пример теперь выглядит так:
public static void main(String[] args) {
List<Animal> animalList = new ArrayList<>();
List<Mammal> mammalList = new ArrayList<>();
List<Predator> predatorList = new ArrayList<>();
List<Lion> lionList = new ArrayList<>();
List<AfricanLion> africanLionList = new ArrayList<>();
doWork(animalList); // код будет правильно компилироваться и работать
doWork(mammalList); // код будет правильно компилироваться и работать
doWork(predatorList); // код будет правильно компилироваться и работать
doWork(lionList); // ошибка компиляции "incompatible types"
doWork(africanLionList);// ошибка компиляции "incompatible types"
}
private static void doWork(List<? super Predator> list) {
// work
}Теперь, аргументами метода doWork() могу быть списки с объектами типа Predator, Mammal, Animal, т.е. указанный тип или его предки.
На диаграмме контравариантность обобщенных типов выглядит так:

!!! Ограничения на действия с объектами коллекций вариантных типов
В целях безопасности на действия с вариантными объектами наложены ограничения которые следует учитывать.
Например из списков ковариантного типа можно только читать данные, и только переданный тип и его предков, а записывать в такой список нельзя ничего.
private static void doWorkCovariance(List<? extends Predator> list) {
Object a = list.get(0); // код будет правильно компилироваться и работать
Animal animal = list.get(0); // к��д будет правильно компилироваться и работать
Mammal mammal = list.get(0); // код будет правильно компилироваться и работать
Predator predator = list.get(0); // код будет правильно компилироваться и работать
Lion lion = list.get(0); // ошибка компиляции "incompatible types"
AfricanLion africanLion = list.get(0); // ошибка компиляции "incompatible types"
list.add(new Animal()); // ошибка компиляции "incompatible types"
list.add(new Mammal()); // ошибка компиляции "incompatible types"
list.add(new Predator()); // ошибка компиляции "incompatible types"
list.add(new Lion()); // ошибка компиляции "incompatible types"
list.add(new AfricanLion()); // ошибка компиляции "incompatible types"
}У контравариантных списков можно только добавлять объекты указанного типа и его наследников, а вот прочитать из списка можно только объект типа Object.
private static void doWorkContravariance(List<? super Predator> list) {
Object a = list.get(0); // код будет правильно компилироваться и работать
Animal animal = list.get(0); // ошибка компиляции "incompatible types"
Mammal mammal = list.get(0); // ошибка компиляции "incompatible types"
Predator predator = list.get(0); // ошибка компиляции "incompatible types"
Lion lion = list.get(0); // ошибка компиляции "incompatible types"
AfricanLion africanLion = list.get(0); // ошибка компиляции "incompatible types"
list.add(new Animal()); // ошибка компиляции "incompatible types"
list.add(new Mammal()); // ошибка компиляции "incompatible types"
list.add(new Predator()); // код будет правильно компилироваться и работать
list.add(new Lion()); // код будет правильно компилироваться и работать
list.add(new AfricanLion()); // код будет правильно компилироваться и работать
}В таблице приведены все ограничения для вариантных объектов
Тип | = | get | add |
Инвариантный List<Type> | только List<Type> | Type и предки Type | Type и наследники Type |
Ковариантный List<? extends Type> | List<Type> и List наследников Type | Type и предки Type | ничего |
Контравариантный List<? super Type> | List<Type> и List предков Type | только Object | Type и наследники Type |
Это собственно все что я хотел сказать по этому вопросу. Если материал окажется кому-то полезен, буду рад)
Ссылка для скачивания шпаргалки по данной статье в формате pdf
