Полиморфизм, абстрактные классы и интерфейсы.

20-01-2020

Слово «полиморфизм» означает «много форм». Оно произошло от греческого слова «поли» (много) и «морфос» (что означает форму). Например, в химии углерод проявляет полиморфизм, поскольку он может быть найден в более, чем одной форме: графит и бриллиант. Каждая из форм имеет свои отдельные свойства. В программировании полиморфизм – это возможность объектов с одинаковой спецификацией иметь различную реализацию. В Java полиморфизм реализуется посредством перегрузки и переопределения методов. В соответствии с принципом полиморфизма рекомендуется писать программы на основе общего интерфейса вместо конкретных реализаций.

Подстановка

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

Circle c1 = new Cylinder(5.0);

Теперь можно вызывать все методы, определенные в классе Circle, для ссылки на c1 (который все еще представляет объект Cylinder). Это возможно потому, что объект подкласса обладает всеми свойствами суперкласса. Однако невозможно вызывать методы, определенные в классе Cylinder, для ссылки на c1. Это происходит потому, что c1 – это ссылка на класс Circle, который не знает о методах, определенных в классе Cylinder.

Резюме

  1. Объекты суперкласса могут быть замещены объектами подкласса.
  2. При такой замене мы можем вызывать методы, определенные в суперклассе, и не можем вызывать методы, определенные только в подклассе.
  3. Однако, если в подклассе переопределены унаследованные методы из суперкласса, будут вызваны переопределенные версии методов подкласса.

Апкастинг и даункастинг

Замена объекта суперкласса объектом подкласса называется «апкастингом» (англ. upcasting) или приведением к базовому типу. Апкастинг всегда безопасен, так как объект подкласса обладает всеми свойствами суперкласса и может делать все,что может делать суперкласс.Компилятор проверяет правильность апкастинга и в противном случае выдает ошибку «несовместимости типов».

Даункастинг (англ. downcasting) возвращает замещенный объект к определению через подкласс, т.е. даункастинг – это приведение объекта суперкласса к объекту подкласса.

Circle c1 = new Cylinder(5.0); // апкастинг - безопасен
Cylinder aCylinder = (Cylinder) c1; //доункастинг требует явного приведения типов.

Даункастинг требует оператора явного приведения типов в форме префиксного оператора (новый_тип). Даункастинг не всегда безопасен и вызывает ошибку ClassCastException во время исполнения, если объект даункастинга не принадлежит правильному подклассу.

Оператор “instanceof”

В Java имеется оператор instanceof типа boolean, который возвращает значение true, если объект является экземпляром данного класса.

ИмяОбъекта instanceof ИмяКласса

Экземпляр подкласса также является экземпляром суперкласса.

Резюме по полиморфизму

  1. Объект подкласса выполняет все операции над полями своего суперкласса. Объект суперкласса может быть заменен объектом подкласса. Другими словами, ссылка на класс может содержать объект этого класса или объект одного из подклассов – это называется подстановкой или замещением.
  2. Если значение объекта подкласса присваивается ссылке на суперкласс, то можно вызывать только методы, определенные в суперклассе. Нельзя вызывать методы, определенные в подклассе.
  3. Замененное значение сохраняет свою идентичность в переопределенных методах и скрытых переменных. Если подкласс переопределяет методы в суперклассе, то будет выполняться версия подкласса вместо версии суперкласса.

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

АБСТРАКТНЫЕ КЛАССЫ И ИНТЕРФЕЙСЫ

Абстрактный метод

Абстрактный метод – это метод, имеющий только сигнатуру (т.е. имя метода, список параметров и тип возвращаемого значения) без реализации (т.е. без тела метода). Чтобы объявить абстрактный метод, используется ключевое слово abstract. Например, в классе Shape мы можем объявить абстрактный метод getArea() следующим образом:

abstract public class Shape {
  ...
  public abstract double getArea();
}

Реализация этого метода невозможна в классе Shape, поскольку фактическая фигура еще не известна. (Как вычислить площадь, если фигура неизвестна?). Реализация этого абстрактного метода будет представлена позже, когда фактическая фигура будет известна. Абстрактные методы не могут быть вызваны, поскольку они не имеют реализации (имеют только сигнатуру).

Абстрактный класс

Класс, содержащий один или более абстрактных методов, называется абстрактным классом. Абстрактный класс должен быть объявлен с модификатором abstract (см.код выше). На диаграмме UML абстрактные классы и абстрактные методы выделяются курсивом.

Абстрактный класс неполон в своем определении, поскольку реализация его абстрактных методов отсутствует. Следовательно, на его основе нельзя создавать объекты. В противном случае мы будем иметь незавершенный объект с отсутствующим телом метода. Чтобы использовать абстрактный класс, надо унаследовать подкласс от абстрактного класса. В подклассе-наследнике надо переопределить абстрактные методы и предоставить реализации всех абстрактных методов. Теперь подкласс-наследник будет завершен, и от него можно создавать объекты. (Если подкласс не предоставляет реализацию для всех абстрактных методов суперкласса, то подкласс остается абстрактным). Подведем итоги. Абстрактный класс предоставляет шаблон для дальнейшего развития. Цель абстрактного класса – предоставить общий интерфейс (или протокол, или договор, или понимание, или соглашение об именах) для всех своих подклассов. Например, в абстрактном классе Shape вы можете определить абстрактный метод getArea(). Никакая реализация невозможна, поскольку фактическая фигура неизвестна. Однако, указав сигнатуру абстрактных методов, все подклассы обязаны использовать сигнатуры этих методов. Подклассы могут предоставлять правильные реализации. Вместе с применением полиморфизма можно проводить апкастинг объектов подкласса до Shape и программировать на уровне интерфейса. Разделение на интерфейс и реализацию обеспечивает лучший дизайн программного обеспечения и облегчает его расширение. Например, Shape определяет метод с именем getArea(), для которого все подклассы должны предоставить правильные реализации – можно запросить getArea() из любого подкласса Shape, и правильная площадь будет вычислена. Более того, ваше приложение может быть легко расширено для размещения новых фигур (таких, как Circle или Square) путем наследования большего числа подклассов.

Рекомендация: Программировать на уровне интерфейса, а не реализации. (Что означает создание объектов суперкласса, приведение их к объектам подкласса и вызов методов, определенных только в суперклассе.)

Замечания:

  • Абстрактный метод не может быть объявлен как final, поскольку final-метод не может быть переопределен. С другой стороны, абстрактный метод должен быть переопределен в наследнике до того, как будет использован.
  • Абстрактный метод не может иметь модификатор private (это приведет к ошибке компиляции). Это потому, что private метод невидим для подкласса и, таким образом, не может быть переопределен.

Интерфейс

Интерфейс в Java – это 100% абстрактный суперкласс, который определяет множество методов, которые его подклассы должны поддерживать. Интерфейс содержит только public abstract методы (методы с сигнатурой и без реализации) и, возможно, константы (public static final).

Для определения интерфейса следует использовать ключевое слово “interface” (вместо class для обычных классов). Ключевые слова public и abstract не требуются для абстрактных методов, так как они обязательны по определению.

Интерфейс – это договор о том, что классы могут делать. Он, однако, не указывает, как классу надо это делать.

Соглашение об именах: Используйте причастие (на английском языке), состоящее из одного или нескольких слов. Каждое слово должно начинаться с заглавной буквы, например, Serializable, Movable, Clonable, Runnable и т.д.

Чтобы унаследовать подклассы из интерфейса, надо использовать новое ключевое слово “implements” вместо “extends” для наследуемых подклассов как для обычного, так и для абстрактного классов. Важно отметить, что подкласс, наследующий интерфейс, должен переопределить все абстрактные методы, определенные в интерфейсе. В противном случае подкласс не может быть откомпилирован.

Реализация множественных интерфейсов

Как уже упоминалось, Java поддерживает только единичное наследование. Так, подкласс может быть наследником одного и только одного класса. Java не поддерживает множественное наследование во избежание наследования конфликтующих свойств из множественных суперклассов. Множественное наследование, однако, имеет место в программировании на Java.

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

Например,

public class Circle extends Shape implements Movable, Adjustable {
  // наследует от одного суперкласса, но реализует множественные интерфейсы
  ...
}

Формальный синтаксис интерфейса:

[public|protected|package] interface имяИнтерфейса [extends имяCуперинтерфейса] {
  //константы
  static final ...;

  // сигнатуры абстрактных методов
  ...
}

Все методы в интерфейсе должны быть public и abstract (по определению). Нельзя использовать другие модификаторы доступа, такие как private, protected и default, или такие модификаторы, как static, final. Все поля могут иметь модификаторы public, static и final (по определению). Интерфейс может быть наследником суперинтерфейса. В обозначениях UML используются сплошная линия, связывающая подкласс с конкретным или абстрактным суперклассом и пунктирная линия со стрелкой к интерфейсу. Абстрактные классы и абстрактные методы изображаются курсивом.

1

Зачем использовать интерфейсы?

Интерфейс – это контракт (или протокол, или договор о взаимопонимании) о том, что классы могут делать. Когда класс реализует определенный интерфейс, он гарантирует реализовать все абстрактные методы, объявленные в интерфейсе. Интерфейс определяет множество общих поведений. Классы, реализующие интерфейс, соглашаются на эти поведения и предлагают собственную реализацию этих поведений.Одним из главных применений интерфейса является предложение контракта взаимодействия для двух объектов. Как известно, класс реализует интерфейс, класс содержит конкретные реализации методов, объявленных в этом интерфейсе, и гарантируется возможность вызывать эти методы безопасно. Другими словами, два объекта могут взаимодействовать на основе контракта, определенного в интерфейсе, вместо специфических реализаций. Java не поддерживает множественного наследования, как, например, C++. Множественное наследование позволяет наследовать подкласс более чем от одного суперкласса. Это вызывает проблему двух суперклассов, имеющих конфликтующие реализации. (Какой реализации следовать в подклассе?) Однако в Java множественноенаследование имеет место. Java выполняет это разрешением «реализовать» более одного интерфейса (но наследовать (“extends”) можно только от единственного суперкласса). Поскольку интерфейсы содержат только абстрактные методы без реализаций, никакого конфликта не возникает между множественными интерфейсами. (Интерфейс может содержать константы, но это не рекомендуется; если подкласс реализует два интерфейса с конфликтующими константами, компилятор выдаст ошибку компиляции.)

Интерфейс и абстрактный суперкласс

Вопрос о том, как лучше проектировать – с использованием интерфейса или абстрактного суперкласса – не имеет ясного ответа. Рекомендуется использовать абстрактный суперкласс, если имеется четкая иерархия классов. Абстрактный класс может содержать частичную реализацию (например, переменные и методы). Интерфейс не может содержать никакой реализации, но просто определяет поведение.

Динамическое (позднее) связывание

Часто рассматриваются объекты не типа своего класса, но их базового типа (суперкласса или интерфейса). Это позволяет писать коды, не зависящие от конкретного типа в реализации. В примере для Shape мы всегда можем использовать getArea() и не волноваться по поводу того, являются ли объекты треугольниками или кругами. Это, однако, создает новую проблему. Во время компиляции компилятор не может точно знать, какой именно фрагмент кода связывается с объектом во время выполнения, другими словами, например, getArea() имеет различные реализации для Rectangle и Triangle.

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

Для поддержки полиморфизма объектно ориентированный язык использует иной механизм, называемый динамическим связыванием (или поздним связыванием, или связыванием во время выполнения). При вызове метода исполняемый код определяется только во время выполнения. Во время компиляции компилятор проверяет, существует ли метод, и выполняет проверку типа по аргументам и типу возвращаемого значения, однако не знает, какой именно фрагмент кода выполнится при выполнении. Когда сообщение посылается объекту, чтобы вызвать метод, объект определяет, какой именно код будет выполняться. Несмотря на то что динамическое связывание разрешает проблему поддержки полиморфизма, оно вызывает новую проблему. Компилятор не способен проверить правильность приведения типов. Правильность приведения типов может быть проверена только во время выполнения (посредством исключения ClassCastException, генерируемого в случае несоответствия типов). Разрешить эту проблему позволяют дженерики.

Инкапсуляция, связывание и связность

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

Инкапсуляция требует содержать данные и методы внутри класса, чтобы пользователи не имели доступа к данным напрямую, но только посредством методов. Герметичная инкапсуляция может быть достигнута объявлением всех переменных класса с модификатором private и поддержкой public-методов – геттеров и сеттеров для переменных. Преимуществом является то, что при этом вы имеете полный контроль над тем, как данные должны быть прочитаны (например, в каком формате) и каким образом данные будут изменены (например, при проверке).

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

Связывание относится к степени, с которой один класс зависит от знания внутреннего устройства другого класса. Герметичное связывание нежелательно,так как,если один класс изменяет свое внутреннее представление, все другие тесно связанные классы должны быть переписаны. Очевидно, избегание связывания часто ассоциируется с герметичной инкапсуляцией. Например, использование хорошо определенного public-метода для доступа к данным вместо прямого доступа к данным.

Связность относится к степени, с которой класс или метод противостоит разрушению на мелкие части. Желательна высокая степень связности. Каждый класс должен быть спроектирован таким образом, чтобы моделировать единую сущность со своим множеством ответственностей и выполнять тесно связанные задачи. А каждый метод должен выполнять единственную задачу. Классы с низкой связностью трудно поддерживать и повторно использовать.

Итак, высокая связность ассоциируется с избеганием связывания. Это происходит потому, что класс с высокой связностью имеет меньшее (или минимальное) взаимодействие с другими классами.

Литература

  1. О.И. Гуськова, "ООП в Java", Москва, 2018