Вариантность обобщенных типов в картинках и простых примерах

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

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

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

Самый простой вариант понять эти принципы на примере коллекций. Для примера используем пять классов которые последовательно наследуются друг от друга и коллекции ArrayList предназначенные для хранения экземпляров этих типов.

95f2cd6a20b69cbd9af6e644751cc77a.png

List      animalList       = new ArrayList<>();
List      mammalList       = new ArrayList<>();
List    predatorList     = new ArrayList<>();
List        lionList         = new ArrayList<>();
List africanLionList  = new ArrayList<>();

Принцип инвариантности (Invariance).

Данный принцип подразумевает неизменность форм. В отношении обобщенных типов это говорит о жёсткой привязке требуемых данных к конкретным типам.

Примером этого служит жестко заданный тип данных хранящихся в коллекции.

List predatorList;

Для демонстрации используем список List с жёстко заданным типом данных, и попробуем присвоить ему ссылку на список с данными классов предков и наследников.

public static void main(String[] args) {
        List      animalList       = new ArrayList<>();
        List      mammalList       = new ArrayList<>();
        List    predatorList     = new ArrayList<>();
        List        lionList         = new ArrayList<>();
        List africanLionList  = new ArrayList<>();

        List predatorList1 = predatorList;	// код будет правильно компилироваться и работать
        List predatorList2 = animalList;		// ошибка компиляции "incompatible types"
        List predatorList3 = mammalList;		// ошибка компиляции "incompatible types"
        List predatorList4 = lionList;		// ошибка компиляции "incompatible types"
        List predatorList5 = africanLionList;	// ошибка компиляции "incompatible types"
}

Присвоить списку List удалось только ссылку на коллекцию с данными типа Predator, не смотря на то что другие коллекции содержат данные типы которых являются наследниками или предками указанного типа. Соответственно, операция присваивания, является инвариантной к типу данных.

Второй пример демонстрирует принцип инвариантности аргумента метода doWork(list)

public static void main(String[] args) {
        List      animalList       = new ArrayList<>();
        List      mammalList       = new ArrayList<>();
        List    predatorList     = new ArrayList<>();
        List        lionList         = new ArrayList<>();
        List 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 list) {
        // work
    }

Как и в случае с присваиванием, список передаваемый методу doWork () должен содержать объекты типа Predator  и никаких других вариантов.

На диаграмме инвариантность будет выглядеть так:

ae0085adb2c645137c9aca713f0471f3.png

Принцип Ковариантности (Covariance)

Принцип ковариантности заключается в возможности использовать в качестве данных коллекции содержащие экземпляры объектов наследников или самого указанного класса.

В нашем примере указать, что аргумент является ковариантным, можно с использованием конструкции называемой wildcard и имеющей следующий вид : > .

doWork(List list);

На русском языке эту конструкцию можно осмыслить как: «Неуказанный тип расширяющий класс Predator». Изменив код примера мы получим следующее:

public static void main(String[] args) {
        List      animalList       = new ArrayList<>();
        List      mammalList       = new ArrayList<>();
        List    predatorList     = new ArrayList<>();
        List        lionList         = new ArrayList<>();
        List africanLionList  = new ArrayList<>();

        doWork(animalList);		// ошибка компиляции "incompatible types"
        doWork(mammalList);		// ошибка компиляции "incompatible types"
        doWork(predatorList);	// код будет правильно компилироваться и работать
        doWork(lionList);		// код будет правильно компилироваться и работать
        doWork(africanLionList);// код будет правильно компилироваться и работать
    }
    private static void doWork(List list) {
        // work
    }

Следовательно в виде аргумента в метод doWork () могут быть переданы списки с типом данных Predator, Lion, AfricanLion т.е. указанный тип и наследники.

на диаграмме ковариантность обобщенных типов выглядит так:

323e0feb2ea5dde8f1ad4defb62cc712.png

Принцип контравариантности (Contravariance)

Принцип контравариантности прямо противоположен принципу ковариантности. Из чего следует, что относительно нашего примера в качестве данных можно использовать коллекции содержащие данные типов указанного класса или его предков.

Для обозначения контравариантного типа используется подобная ковариантной конструкция, но с ключевым словом super

doWork(List list)

Соответственно, это: Любой неуказанный тип являющийся предком класса Predator.

И наш пример теперь выглядит так:

public static void main(String[] args) {
        List      animalList       = new ArrayList<>();
        List      mammalList       = new ArrayList<>();
        List    predatorList     = new ArrayList<>();
        List        lionList         = new ArrayList<>();
        List africanLionList  = new ArrayList<>();

        doWork(animalList);		// код будет правильно компилироваться и работать
        doWork(mammalList);		// код будет правильно компилироваться и работать
        doWork(predatorList);	// код будет правильно компилироваться и работать
        doWork(lionList);		// ошибка компиляции "incompatible types"
        doWork(africanLionList);// ошибка компиляции "incompatible types"
    }
    private static void doWork(List list) {
        // work
    }

Теперь, аргументами метода doWork () могу быть списки с объектами типа  Predator, Mammal, Animal, т.е. указанный тип или его предки.

На диаграмме контравариантность обобщенных типов выглядит так:

770c4cc651d8a1ed0165087fda634498.png

!!! Ограничения на действия с объектами коллекций вариантных типов

В целях безопасности на действия с вариантными объектами наложены ограничения которые следует учитывать.

Например из списков ковариантного типа можно только читать данные, и только переданный тип и его предков, а записывать в такой список нельзя ничего.

private static void doWorkCovariance(List 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 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

только

List

Type и предки Type

Type и наследники Type

Ковариантный

List

List

и List наследников Type

Type и предки Type

ничего

Контравариантный

List

List

и List предков Type

только Object

Type и наследники Type

Это собственно все что я хотел сказать по этому вопросу. Если материал окажется кому-то полезен, буду рад)

Ссылка для скачивания шпаргалки по данной статье в формате pdf

© Habrahabr.ru