Back end IT news

О концепции ковариантности возвращаемых типов1 min read

05.03.2020 3 min read

О концепции ковариантности возвращаемых типов1 min read

Reading Time: 3 minutes

Хорошо известно, что Java – объектно-ориентированный язык. Однако многие программисты, которые начинают работать с Java, пишут этот код не в объектно-ориентированном стиле. В программах новичков часто присутствует сильная зависимость между классами, которую можно избежать при более тщательном продумывании иерархии сущностей. 

Ещё одной «детской» болезнью является чрезмерное увлечение наследованием в ущерб обычной композиции. Эти и многие другие негативные явления связаны с недостаточным пониманием некоторых аспектов объектно-ориентированного программирования в целом и их реализации на языке Java в частности. Одним из таких аспектов является концепция ковариантности возвращаемых типов (появилась в JAVA SE5), речь о которой пойдёт ниже.

Поставим вопрос: «Как может изменяться тип возвращаемого значения при переопределении метода?» 

Если речь идёт о простых типах, то ответ знают почти все: тип возвращаемого значения переопределённого метода должен быть в точности таким же, как тип возвращаемого значения переопределяемого метода. Даже попытка заменить тип возвращаемого значения типом с более узким диапазоном значений приведёт к ошибке компиляции.

Пример 1

Совсем по-другому обстоят дела, когда тип, возвращаемый переопределяемым методом не является простым. Рассмотрим следующую иерархию классов.

Пример 2

Прежде всего, введём в рассмотрение иерархию фабрик, каждая из которых имеет единственный метод produce() для производства продукции соответствующего типа.

Пример 3

Очевидно, что для всех методов produce() в предложенной иерархии фабрик можно было бы выбрать типом возвращаемого значения Product: тип, который возвращает фабрика, находящаяся на вершине иерархии. Однако не является ли более логичной ситуация, когда Молочная фабрика (MilkFactory) производит именно Молоко (Milk), а не некий абстрактный продукт Product? Аналогично, вполне естественно ожидать, что Фабрика сладостей (SweetsFaсtory) производит сладости (Sweets), а Шоколадная фабрика (ChocolateFactory) производит Шоколад (Chocolate). 

Если взглянуть на код ПРИМЕРА 3, то становится ясно, что ожидания вполне оправдываются, и Java позволяет задать типы возвращаемых значений именно так, как описано выше. Мы видим, что переопределённый метод класса-наследника может возвращать значение, чей тип является наследником того, что возвращает переопределяемый метод класса-родителя. В этом и состоит концепция ковариантности возвращаемых типов. Интересно отметить, что в предыдущих версиях Java эта концепция не работала.

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

ВОПРОС:  Может ли метод produce() класса ChocolateFactory использовать Product в качестве типа возвращаемого значения? (код других классов ПРИМЕРА 3 предполагается неизменным)

ОТВЕТ: Нет, компилятор такого не допустит. Дело в том, что родительским классом для ChocolateFactory является класс SweetsFactory. Метод produce() этого класса возвращает Sweets, а класс Product не является подклассом Sweets. Однако метод produce() класса ChocolateFactory вполне может использовать класс Chocolate в качестве типа возвращаемого значения (как это, собственно, и сделано в ПРИМЕРЕ3). Это допустимо, так как класс Chocolate является подклассом Sweets. Также для указанного метода есть возможность возвращать непосредственно  Sweets.

ВОПРОС: Может ли метод produce() класса MilkFactory использовать класс Chocolate в качестве типа возвращаемого значения? (код других классов ПРИМЕРА 3 предполагается неизменным)

ОТВЕТ: Да, синтаксически это вполне допустимо (хотя и не вполне логично с точки зрения наименований). Дело в том, что класс Chocolate является наследником класса Product, пусть и не прямым. Поэтому срабатывает концепция ковариантности возвращаемых типов.

Что в итоге

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

Антон Мальцев, специально для ITEA. 24 февраля 2020 г.

За нашими плечами 5 лет практики в обучении IT технологиям, мы давно изучаем рынок и наша цель - помочь каждому найти себя в IT cфере и предоставить ему самый кротчайший путь к достижению целей