Что такое ковариантность типов
Понятия ковариантности и контравариантности связаны с возможностью использовать в приложении вместо некоторого типа другой тип, который находится ниже или выше в иерархии наследования.
Имеется три возможных варианта поведения:
- Ковариантность : позволяет использовать более конкретный тип, чем заданный изначально
- Контравариантность : позволяет использовать более универсальный тип, чем заданный изначально
- Инвариантность : позволяет использовать только заданный тип
C# позволяет создавать ковариантные и контравариантные обобщенные интерфейсы. Эта функциональность повышает гибкость при использовании обобщенных интерфейсов в программе. По умолчанию все обобщенные интерфейсы являются инвариантными.
Для рассмотрения ковариантных и контравариантных интерфейсов возьмем следующие классы:
class Message < public string Text < get; set; >public Message(string text) => Text = text; > class EmailMessage : Message < public EmailMessage(string text): base(text) < >>
Здесь определен класс сообщения Message, который получает через конструктор текст и сохраняет его в свойство Text. А класс EmailMessage представляет условное email-сообщение и просто вызывает конструктор базового класса, передавая ему текст сообщения.
Ковариантные интерфейсы
Обобщенные интерфейсы могут быть ковариантными, если к универсальному параметру применяется ключевое слово out . Такой параметр должен представлять тип объекта, который возвращается из метода. Например:
interface IMessenger < T WriteMessage(string text); >class EmailMessenger : IMessenger < public EmailMessage WriteMessage(string text) < return new EmailMessage($"Email: "); > >
Здесь обобщенный интерфейс IMessenger представляет интерфейс мессенджера и определяет метод WriteMessage() для создания сообщения. При этом на момент определения интерфейса мы не знаем, объект какого типа будет возвращаться в этом методе. Ключевое слово out в определении интерфейса указывает, что данный интерфейс будет ковариантным.
Класс EmailMessenger, который представляет условную программу для отправки email-сообщений, реализует этот интерфейс и возвращает из метода WriteMessage() объект EmailMessage.
Применим данные типы в программе:
IMessenger outlook = new EmailMessenger(); Message message = outlook.WriteMessage("Hello World"); Console.WriteLine(message.Text); // Email: Hello World IMessenger emailClient = new EmailMessenger(); IMessenger messenger = emailClient; Message emailMessage = messenger.WriteMessage("Hi!"); Console.WriteLine(emailMessage.Text); // Email: Hi!
То есть мы можем присвоить более общему типу IMessenger объект более конкретного типа EmailMessenger или IMessenger .
В то же время если бы мы не использовали ключевое слово out :
interface IMessenger
то мы столкнулись бы с ошибкой в строке
IMessenger outlook = new EmailMessenger(); // ! Ошибка IMessenger emailClient = new EmailMessenger(); IMessenger messenger = emailClient; // ! Ошибка
Поскольку в этом случае невозможно было бы привести объект IMessenger к типу IMessenger
При создании ковариантного интерфейса надо учитывать, что универсальный параметр может использоваться только в качестве типа значения, возвращаемого методами интерфейса. Но не может использоваться в качестве типа аргументов метода или ограничения методов интерфейса.
Контравариантные интерфейсы
Для создания контравариантного интерфейса надо использовать ключевое слово in . Например, возьмем те же классы Message и EmailMessage и определим следующие типы:
interface IMessenger < void SendMessage(T message); >class SimpleMessenger : IMessenger < public void SendMessage(Message message) < Console.WriteLine($"Отправляется сообщение: "); > >
Здесь опять же интерфейс IMessenger представляет интерфейс мессенджера и определяет метод SendMessage() для отправки условного сообщения. Ключевое слово in в определении интерфейса указывает, что этот интерфейс — контравариантный .
Класс SimpleMessenger представляет условную программу отправки сообщений и реализует этот интерфейс. Причем в качестве типа используемого этот класс использует тип Message. То есть SimpleMessenger фактически представляет тип IMessenger .
Применим эти типы в программе:
IMessenger outlook = new SimpleMessenger(); outlook.SendMessage(new EmailMessage("Hi!")); IMessenger telegram = new SimpleMessenger(); IMessenger emailClient = telegram; emailClient.SendMessage(new EmailMessage("Hello"));
Так как интерфейс IMessenger использует универсальный параметр с ключевым словом in , то он является контравариантным, поэтому в коде мы можем переменной типа IMessenger передать объект IMessenger или SimpleMessenger
Если бы ключевое слово in не использовалось бы, то мы не смогли бы это сделать. То есть объект интерфейса с более универсальным типом приводится к объекту интерфейса с более конкретным типом.
При создании контрвариантного интерфейса надо учитывать, что универсальный параметр контрвариантного типа может применяться только к аргументам метода, но не может применяться к возвращаемому результату метода.
Совмещение ковариантности и контравариантности
Также мы можем совместить ковариантность и контравариантность в одном интерфейсе. Например:
interface IMessenger < void SendMessage(T message); K WriteMessage(string text); >class SimpleMessenger : IMessenger < public void SendMessage(Message message) < Console.WriteLine($"Отправляется сообщение: "); > public EmailMessage WriteMessage(string text) < return new EmailMessage($"Email: "); > >
Фактически здесь объединены два предыдущих примера. Благодаря ковариантности/контравариантности объект класса SimpleMessenger может представлять типы IMessenger , IMessenger , IMessenger и IMessenger . Применение классов:
IMessenger messenger = new SimpleMessenger(); Message message = messenger.WriteMessage("Hello World"); Console.WriteLine(message.Text); messenger.SendMessage(new EmailMessage("Test")); IMessenger outlook = new SimpleMessenger(); EmailMessage emailMessage = outlook.WriteMessage("Message from Outlook"); outlook.SendMessage(emailMessage); IMessenger telegram = new SimpleMessenger(); Message simpleMessage = telegram.WriteMessage("Message from Telegram"); telegram.SendMessage(simpleMessage);
Что такое ковариантность и контравариантность?
Формально, ковариантность/контравариантность типов – это сохранение/обращение порядка наследования для производных типов. Проще говоря, когда у ковариантных сущностей типами-параметрами являются родитель и наследник, они сами становятся как бы родителем и наследником. Контравариантные наоборот, становятся наследником и родителем.
Легче всего осознать эти понятия на примерах:
Ковариантность: List можно присвоить в переменную типа List (как будто он наследник List ).
Контравариантность: в качестве параметра метода List#sort типа Comparator может быть передан Comparator (как будто он родитель Comparator )
Отношение типов «можно присвоить» – не совсем наследование, такие типы называются совместимыми (отношение «is a»).
Существует еще одно связанное понятие – инвариантность. Инвариантность – это отсутствие свойств ковариантности и контрвариантности. Дженерики без вайлдкардов инвариантны: List нельзя положить ни в переменную типа List , ни в List .
Массивы ковариантны: в переменную Object[] можно присвоить значение типа String[] .
Переопределение методов начиная с Java 5 ковариантно относительно типа результата и типов исключений.
Вариантность в программировании
До сих пор не можете спать, пытаясь осмыслить понятия ковариантности и контравариантности? Чувствуете, как они дышат вам в спину, но когда оборачиваетесь ничего не находите? Есть решение!
Меня зовут Никита, и сегодня мы попытаемся заставить механизм в голове работать корректно. Вас ожидает максимально доступное рассмотрение темы вариантности в примерах. Добро пожаловать под кат.
Брифинг
Вариантность в данном посте разбирается безотносительно к какому-либо языку программирования. Примеры в разделе практики написаны на псевдоязыке (он чудом оказался похож на C#) и поэтому не обязаны компилироваться вашим любимым компилятором. Приступим.
Хитрости терминологии
В документации, технической литературе и других источниках вы могли встречаться с различными названиями для явлений вариантности. Больше не стоит пугаться и путаться.
Термины ковариантность и ковариация эквивалентны (по крайней мере в программировании). Более того, термины контравариантность и контравариация также эквивалентны. Так, например, термины ковариантность и контравариантность используется в Википедии и у Троелсена (в переводе). А термины ковариация и контравариация встречаются, например, на MSDN и у Скита (в переводе).
В английском языке всё проще — covariance и contravariance.
Теория
Вариантность — перенос наследования исходных типов на производные от них типы. Под производными типами понимаются контейнеры, делегаты, обобщения, а не типы, связанные отношениями «предок-потомок». Различными видами вариантности являются ковариантность, контравариантность и инвариантность.
Ковариантность — перенос наследования исходных типов на производные от них типы в прямом порядке.
Контравариантность — перенос наследования исходных типов на производные от них типы в обратном порядке.
Инвариантность — ситуация, когда наследование исходных типов не переносится на производные.
Если у производных типов наблюдается ковариантность, говорят, что они ковариантны исходному типу. Если у производных типов наблюдается контравариантность, говорят, что они контравариантны исходному типу. Если у производных типов не наблюдается ни того, ни другого, говорят, что они инвариантны.
Вот и всё, что нужно знать. Конечно, тем кто первый раз сталкивается с вариантностью, трудно вникнуть. Поэтому рассмотрим конкретные примеры.
Практика
Для чего всё это?
Вся суть вариантности состоит в использовании в производных типах преимуществ наследования. Известно, что если два типа связаны отношением «предок-потомок», то объект потомка может храниться в переменной типа предка. На практике это значит, что мы можем использовать для каких-либо операций объекты потомка вместо объектов предка. Тем самым, можно писать более гибкий и короткий код для выполнения действий поддерживаемых разными потомками с общим предком.
Исходная иерархия и производные типы
Для начала опишем иерархию типов, которой будем оперировать. Вверху иерархии у нас находится Device (устройство), потомками которого являются Mouse (мышь), Keyboard (клавиатура). У Mouse в свою очередь тоже есть потомки — WiredMouse (проводная мышь), WirelessMouse (беспроводная мышь).
Все любят контейнеры. На их примере наиболее просто объяснить, что подразумевается под производными типами. Если говорить о списках как производных типах, то для типа Device производным будет
List (список устройств). Аналогично, для типа Keyboard производным будет List (список клавиатур). Думаю, если и были сомнения, то теперь их нет.
Классическая ковариантность
Ковариантность также легче изучать на примере контейнеров. Для этого выделим часть иерархии (ветвь) — Keyboard : Device (клавиатура является устройством, клавиатура частный случай устройства). Опять возьмём списки и построим ковариантную производную ветвь — List : List (список клавиатур является частным случаем списка устройств). Как видим, наследование передалось в прямом порядке.
Рассмотрим пример кода. Есть функция, которая принимает список устройств List и совершает над ними какие-то манипуляции. Как вы уже догадались, в эту функцию можно передать список клавиатур List :
void DoSmthWithDevices(List devices) < /* действия с элементами списка */ >. List keyboards = new List < /* заполнение списка */ >; DoSmthWithDevices(keyboards);
Классическая контравариантность
Каноническим для изучения контравариантности является рассмотрение её на основе делегатов. Допустим, у нас есть обобщённый делегат:
delegate void Action(T something);
Для исходного типа Device производным будет Action , а для Keyboard — Action . Полученные делегаты могут представлять функции, которые выполняют какие-то действия над устройством или мышью соответственно. Для ветви Keyboard : Device построим производную контравариантную ветвь — Action : Action (действие над устройством является частным случаем действия над клавиатурой — звучит странно, но так и есть). Если можно нажать клавишу на клавиатуре, то это не значит, что и на устройстве можно нажать её (оно может не иметь понятия о том, что такое клавиша). Но если можно подключить устройство, то можно этим же способом (методом, функцией) подключить и клавиатуру. Как видим, наследование передалось в обратном порядке.
Из выше сказанного логично, что если функция может выполнить, что-то над устройством, то она может выполнить это и над клавиатурой. Это значит, мы можем передать объект делегата Action в функцию, принимающую объект делегата Action . Рассмотрим в коде:
void DoSmthWithKeyboard(Action actionWithKeyboard) < /* выполнение actionWithKeyboard над клавиатурой */ >. Action actionWithDevice = device => device.PlugIn(); DoSmthWithKeyboard(actionWithDevice);
Немного инвариантности
Если производные типы инвариантны к исходным типам, то для ветви Keyboard : Device не образуется ни ковариантной ( List : List ), ни контравариантной ( Action : Action ) ветви. Это значит, что нет никакой связи между производными типами. Как видим, наследование не переносится.
А что если?
Неочевидная ковариантность
Делегаты типа Action могут быть ковариантны. Это значит, что для ветви Keyboard : Device образуется ковариантная ветвь — Action : Action . Таким образом, в функцию, принимающую объект делегата Action , можно передавать объект делегата Action .
void DoSmthWithDevice(Action actionWithDevice) < /* выполнение actionWithDevice над устройством */ >. Action actionWithKeyboard = keyboard => ((Device)keyboard).PlugIn(); DoSmthWithDevice(actionWithKeyboard);
Неочевидная контравариантность
Контейнеры могут быть контравариантны. Это значит, что для ветви Keyboard : Device образуется контравариантная ветвь — List : List . Таким образом, в функцию, принимающую List , можно передавать List :
void FillListWithKeyboards(List keyboards) < /* заполнение списка клавиатур */ >. List devices = new List(); FillListWithKeyboards(devices);
Сакральный смысл
Рассмотренные выше экзотические виды вариантности имеют, разве что, академическую ценность. Сложно придумать реальную задачу, которая легче решается при наличии такого рода возможностей. Стоит запомнить, что ковариантность и контравариантность могут вызывать ошибки времени выполнения. Для их устранения требуется вводить определённые ограничения. Компиляторы, как правило, такие ограничения не вводят.
Безопасность для контейнеров
Если производный тип ковариантен, то для обеспечения безопасности контейнер должен быть read only. В противном случае, остаётся возможность записать в List объект неверного типа ( Device , Mouse и другие) через приведение к List :
List devices = new List(); devices.Add(new Device()); // ошибка времени выполнения
Если производный тип контравариантен, то для обеспечения безопасности контейнер должен быть write only. В противном случае, остаётся возможность считывания из List объекта неверного типа ( Keyboard , Mouse и других) через приведение к соответствующему списку ( List , List и другим):
List keyboards = new List(); keyboards.Add(new Keyboard()); keyboards[0].PressSpace(); // ошибка времени выполнения
Двойные стандарты для делегатов
Разумным для делегатов является ковариантность для выходного значения и контравариантность для входных параметров (исключая передачу по ссылке). В случае соблюдения данных условий ошибок времени выполнения не возникает.
Дебрифинг
Представленных примеров достаточно для понимания принципов работы вариантности. Данные о её поддержке разными типами вашего любимого языка ищите в соответствующей спецификации. Если что-то пошло не так — закройте глаза, выдохните и выпейте чай. После этого попытайтесь снова. Спасибо за внимание.
UDP
Возможно более правильным определением вариантности является предложенное Эриком Липпертом. Спасибо Alex_sik за ссылку на статью.
Совместимость присваивания, assignment compatibility — это возможность присвоить значение более частного типа совместимой переменной более общего типа.
Вариантность — это сохранение совместимости присваивания исходных типов у производных типов.
Ковариантность — это сохранение совместимости присваивания исходных типов у производных в прямом порядке.
Контравариантность — это сохранение совместимости присваивания исходных типов у производных в обратном порядке.
- ковариантность
- контравариантность
- инвариантность
Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 15
Привет-привет! Как много нужно знать Java разработчику? Можно долго спорить по этому вопросу, но правда в том, что на собеседовании вас будут гонять по теории в полный рост. Даже по тем областям знаний, которым вам не доведется воспользоваться в работе. Ну а если вы новичок, по вашим теоретическим знаниям пройдутся очень серьезно. Раз опыта и больших достижений еще нет, остается только проверить прочность базы знаний. Сегодня мы продолжим заниматься укреплением этой самой базы, разбирая самые популярные вопросы на собеседованиях для Java-разработчиков. Полетели!
Java Core
9. В чем разница между статическим и динамическим связыванием в Java?
На данный вопрос я уже ответил в этой статье в 18 вопросе про статический и динамический полиморфизм, советую ознакомиться.
10. Можно ли использовать private или protected переменные в interface?
- public — интерфейс предоставляет возможность клиенту взаимодействовать с объектом. Если бы переменные не были общедоступными, у клиентов не было бы к ним доступа.
- static — интерфейсы не могут быть созданы (а точнее, их объекты), поэтому переменная статична.
- final — так как интерфейс используется для достижения 100% абстракции, переменная имеет свой конечный вид (и не будет изменена).
11. Что такое Classloader и для чего используется?
Classloader — или Загрузчик классов — обеспечивает загрузку классов Java. А точнее, обеспечивают загрузку его наследники — конкретные загрузчики классов, т.к. сам ClassLoader абстрактен. Каждый раз, когда загружается какой-либо .class-файл, например, после обращения к конструктору или статическому методу соответствующего класса, это действие выполняет один из наследников класса ClassLoader . Есть три вида наследников:
- Bootstrap ClassLoader — базовый загрузчик, реализован на уровне JVM и не имеет обратной связи со средой выполнения, так как является частью ядра JVM и написан в машинном коде. Данный загрузчик служит родительским элементом для всех других экземпляров ClassLoader. В основном отвечает за загрузку внутренних классов JDK, обычно rt.jar и других основных библиотек, расположенных в каталоге $ JAVA_HOME / jre / lib. У разных платформ могут быть разные реализации этого загрузчика классов.
- Extension Classloader — загрузчик расширений, потомок класса базового загрузчика. Заботится о загрузке расширения стандартных базовых классов Java. Загружается из каталога расширений JDK, обычно — $ JAVA_HOME / lib / ext или любого другого каталога, упомянутого в системном свойстве java.ext.dirs (с помощью данной опции можно управлять загрузкой расширений).
- System ClassLoader — системный загрузчик, реализованный на уровне JRE, который заботится о загрузке всех классов уровня приложения в JVM. Он загружает файлы, найденные в переменном окружении классов -classpath или -cp опции командной строки.
Загрузчики классов — это часть среды выполнения Java. В тот момент когда JVM запрашивает класс, загрузчик классов пытается найти класс и загрузить определение класса в среду выполнения, используя полное имя класса. Метод java.lang.ClassLoader.loadClass() отвечает за загрузку определения класса во время выполнения. Он пытается загрузить класс на основе полного имени. Если класс еще не загружен, он делегирует запрос загрузчику родительского класса. Этот процесс происходит рекурсивно выглядит так:
- System Classloader пытается найти класс в своем кеше.
- 1.1. Если класс найден, загрузка успешно завершена.
- 1.2. Если класс не найден, загрузка делегируется к Extension Classloader-у.
- Extension Classloader пытается найти класс в собственном кеше.
- 2.1. Если класс найден — успешно завершена.
- 2.2. Если класс не найден, загрузка делегируется Bootstrap Classloader-у.
- Bootstrap Classloader пытается найти класс в собственном кеше.
- 3.1. Если класс найден, загрузка успешно завершена.
- 3.2. Если класс не найден, базовый Bootstrap Classloader попытается его загрузить.
- Если загрузка:
- 4.1. Прошла успешно — загрузка класса завершена.
- 4.2. Не прошла успешно — управление передается к Extension Classloader.
- 5. Extension Classloader пытается загрузить класс, и если загрузка:
- 5.1. Прошла успешно — загрузка класса завершена.
- 5.2. Не прошла успешно — управление передается к System Classloader.
- 6. System Classloader пытается загрузить класс, и если загрузка:
- 6.1. Прошла успешно — загрузка класса завершена.
- 6.2. Не прошла успешно — генерируется исключение — ClassNotFoundException.
Тема загрузчиков классов обширна и ею не стоит пренебрегать. Чтобы ознакомиться с ней подробнее, советую прочесть эту статью, а мы не будем задерживаться и пойдем дальше.
12. Что такое Run-Time Data Areas?
Run-Time Data Ares — области данных среды выполнения JVM. JVM определяет некоторые области данных времени выполнения, необходимые во время выполнения программы. Одни из них создаются при запуске JVM. Другие являются локальными по отношению к потокам и создаются только при создании потока (и уничтожаются, когда поток уничтожается). Области данных среды выполнения JVM выглядят так:
- PC Register — регистр ПК — локален для каждого потока и содержит адрес инструкции JVM, которую поток выполняет в данный момент.
- JVM Stack — область памяти, которая используется как хранилище для локальных переменных и временных результатов. У каждого потока есть свой отдельный стек: как только поток завершается, этот стек также уничтожается. Стоит отметить, что преимуществом stack над heap является производительность, в то время как heap безусловно имеет преимущество в масштабе хранилища.
- Native Method Stack — область данных для каждого потока, в которой хранятся элементы данных, аналогичные стеку JVM, для выполнения собственных (не Java) методов.
- Heap — используется всеми потоками как хранилище которое содержит объекты, метаданные классов, массивы и т. д., которые создаются во время выполнения. Данная область создается при запуске JVM и уничтожается при завершении ее работы.
- Method area — область метода — эта область времени выполнения общая для всех потоков и создается при запуске JVM. Он хранит структуры для каждого класса, такие как пул констант (Runtime Constant Pool — пул для хранения констант), код для конструкторов и методов, данные метода и т. д.
13. Что такое immutable object?
В данной части статьи в 14 и 15 вопросе уже есть ответ на этот вопрос, поэтому ознакамливаетесь не теряя времени зря.
14. В чем особенность класса String?
Ранее в разборе мы неоднократно говорили про те или иные особенности String (для этого был отдельный раздел). Сейчас же подведем итог по особенностям String :
- Это самый популярный объект в Java, который применяют для разнообразных целей. По частоте использования он не уступает даже примитивным типам.
- Объект данного класса можно создать без использования ключевого слова new — непосредственно через кавычки String str = “строка”; .
- String — это immutable класс: при создании объекта данного класса его данные нельзя изменить (когда вы к некоторой строке добавляете + “другую строку”, как результат вы получите новую, третью строку). Неизменность класса String делает его потокобезопасным.
- Класс String финализирован (имеет модификатор final ), поэтому его наследование невозможно.
- У String есть свой пул строк, область памяти в heap, которая кеширует создаваемые строковые значения. В этой части серии, в 62 вопросе, я описывал строковой пул.
- В Java присутствуют аналоги String , также предназначенные для работы с строками — StringBuilder и StringBuffer , но с тем отличием, что они изменяемые. Подробнее о них вы можете почитать в этой статье.
15. Что такое ковариантность типов?
Для понимания ковариантности мы рассмотрим пример. Предположим, у нас есть класс животного:
public class Animal < void voice() < System.out.println("*тишина*"); >>
И некоторый расширяющий его класс Dog :
public class Dog extends Animal < @Override public void voice() < System.out.println("Гав, гав, гав. "); >>
Как мы помним, родительскому типу мы можем без проблем присваивать объекты типа наследника:
Animal animal = new Dog();
Это у нас будет ничто иное как полиморфизм. Удобно, гибко не так ли? Ну а в случае со списком животных? Сможем ли мы задать списку с дженериком Animal список с объектами Dog ?
List dogs = new ArrayList<>(); List animals = dogs;
В таком случае строка присвоения списку животных списка собак будет подчеркнута красным, т.е. компилятор не пропустит данный код. Несмотря на то, что вроде как это присваивание весьма логично (ведь переменной типа Animal мы можем присвоить объект Dog ) его сделать нельзя. Это происходит потому, что если бы это было допустимо, в список, который изначально предназначен для Dog , мы сможем положить объект Animal , при этом думая, что в списке у нас только Dogs . И потом, к примеру, возьмём с помощью метода get() объект у того списка dogs , думая, что это собака, и вызовем у него некоторый метод объекта Dog , которого нет у Animal . И как вы понимаете, это невозможно — упадет ошибка. Но, к счастью, компилятор не пропускает данную логическую ошибку с присвоением списка потомков, списку родителей (и наоборот). В Java возможно присвоение объектов списков лишь переменным списков с совпадающими дженериками. Это и называется инвариацией. Если бы могли это сделать, это называлось бы и называлось ковариацией. То есть, ковариация — это если бы мы могли переменной типа List
List dogs = new ArrayList<>(); List extends Animal>animals = dogs;
В результате вы увидите в IDE, что компилятор не будет ругаться на данную конструкцию. Давайте проверим работоспособность данной конструкции. Предположим, у нас есть метод, который заставляет всех переданных ему животных издать звуки:
public static void animalsVoice(List animals) < for (Animal animal : animals) < animal.voice(); >>
Передадим ему список с собаками:
List dogs = new ArrayList<>(); dogs.add(new Dog()); dogs.add(new Dog()); dogs.add(new Dog()); animalsVoice(dogs);
В консоли мы увидим следующий вывод:
Гав, гав, гав. Гав, гав, гав. Гав, гав, гав.
А значит данный подход к ковариантности успешно работает. Отмечу, что в список с данным дженериком ? extends Animal мы не можем вставить новые данные никакого типа: ни типа Dog , ни даже типа Animal :
List dogs = new ArrayList<>(); List extends Animal>animals = dogs; animals.add(new Dog()); dogs.add(new Animal());
Собственно, в последних двух строках компилятор будет подчеркивать красным вставку объектов. Это связано с тем, что мы не можем быть на сто процентов уверены, список с объектами какого типа будет присвоен списку с данных дженериком . Хотелось бы ещё рассказать про контравариантность , так как обычно это понятие идет всегда вместе с ковариантностью, и как правило спрашивают о них вместе. Это понятие — некоторая противоположность ковариантности, так как для данной конструкции используется тип наследника. Предположим, нам нужен список, которому можно будет присвоить список с типом объектов, не являющихся предками объекта Dog . При этом мы заранее не знаем, что это будут за конкретные типы. В таком случае нас может выручить конструкция вида ? super Dog , для которой подходят все типы — прародители класса Dog :?>
List animals = new ArrayList<>(); List super Dog>dogs = animals; dogs.add(new Dog()); dogs.add(new Dog());
Мы можем смело добавлять в список с таким дженериком объекты типа Dog , ведь у него в любом случае присутствуют все реализованные методы любого его прародителя. Но мы не сможем добавить объект типа Animal , так как нет уверенности, что внутри будут именно объекты этого типа, а не, например, Dog . Ведь мы можем запросить у элемента данного списка метод класса Dog , которого не будет в наличии у Animal . В таком случае возникнет ошибка компиляции. Также, если бы мы захотели реализовать предыдущий метод, но уже с данным дженериком:
public static void animalsVoice(List dogs) < for (Dog dog : dogs) < dog.voice(); >>
мы бы получили ошибку компиляции в цикле for , так как мы не можем быть уверены, что пришедший список содержит объекты типа Dog и свободно использовать его методы. Если у данного списка мы вызовем метод dogs.get(0); — мы получим объект типа Object . То есть для работы метода animalsVoice() нам как минимум нужно добавить небольшие манипуляции с сужением данных вида:
public static void animalsVoice(List dogs) < for (Object obj : dogs) < if (obj instanceof Dog) < Dog dog = (Dog) obj; dog.voice(); >> >
16. Как есть методы в классе Object?
В данной части серии, в 11 пункте, я уже ответил на данный вопрос, поэтому настоятельно советую ознакомиться, если вы до сих пор этого не сделали. На этом на сегодня и закончим. До встречи в следующей части!
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 1
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 2
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 3
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 4
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 5
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 6
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 7
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 8
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 9
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 10
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 11
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 12
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 13
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 14
- Разбор вопросов и ответов с собеседований на Java-разработчика. Часть 16