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