Ковариантность, контравариантность, инвариантность
Ковариантность, контравариантность, инвариантность

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

Самый простой вариант понять эти принципы на примере коллекций. Для примера используем пять классов которые последовательно наследуются друг от друга и коллекции 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