☕ Разбираем на простых примерах: наследование в Java
Разбираемся в нюансах наследования в языке Java и в том, с какими проблемами можно столкнуться в процессе изучения. Начинающие разработчики часто испытывают трудности в понимании основных парадигм ООП. Статья призвана пролить свет на то, как реализуется наследование в Java. Будут затронуты такие понятия, как родительский и дочерние классы, абстрактные классы, проблема ромба и методы ее решения в Java. После пары вступительных слов перейдем непосредственно к теме статьи. Наследование – механизм большинства ОО-языков программирования, призванный реализовывать полиморфизм. Полиморфизм – один из принципов ООП. Практическим следствием этого является структурированность кода и выделение общей функциональности в отдельный класс. Рассмотрим механизм наследования на примере языка программирования Java. Для примера возьмем классическую игру змейка. Также будем считать, что мир змейки состоит из пикселей. Тогда можно получить следующую структуру: Начальная схема отношения классов Исходя из схемы выше, реализуем 4 класса: Здесь: Таким образом, описав класс Внесем изменения в игру: дадим каждому из пикселей свойство, определяющее может ли он быть съеден или нет. Так как мы не можем определить базовое поведение этого метода, сделаем его абстрактным: abstract – ключевое слово, применяемое к классам и методам. При этом если в классе есть хотя бы один абстрактный метод, то и сам класс должен быть абстрактным. Важно также помнить, что нельзя создавать экземпляры абстрактных классов. Абстрактный метод – метод без реализации. Так как появился абстрактный метод в неабстрактном классе, нужно разрешить конфликт: определить поведение метода, либо сделать класс абстрактным. Исходя из сформулированной выше постановки задачи, можно сделать вывод, что не может существовать объекта класса Реализуем это в коде: Теперь необходимо определить поведение метода Заметим, что у класса При проектировании системы может получиться ситуация, при которой один класс является дочерним к двум другим классам, которые, в свою очередь, дочерние по отношению к третьему. Например, введем для змейки враждебную змейку. Иллюстрация проблемы множественного наследования Получаем дилемму: если объект класса В Java для решения этой проблемы применяются интерфейсы. Интерфейс – это объект языка Java схожий по своей сути с абстрактным классом. Для определения интерфейса в языке есть отдельное ключевое слово interface. Интерфейс не обязан иметь в себе сигнатуры методов. Пустые интерфейсы называются интерфейсами-маркерами. Классическим примером интерфейса-маркера можно считать Если интерфейс так похож на абстрактный класс, зачем тогда в языке присутствуют обе эти конструкции? Для этого есть несколько причин: Создадим интерфейс, определяющий возможность перемещаться, и имплементируем его: Как уже говорилось выше, интерфейс и абстрактный класс очень похожи, и, вместе с этим, у интерфейса есть преимущество: класс может имплементировать множество интерфейсов. Так почему бы не сделать из интерфейса абстрактный класс? Данная функциональность хоть и не лишена смысла, однако, ее следует применять только в крайне ограниченном наборе ситуаций, например, при создании примесей ( Начиная с Java 8, в язык была добавлена возможность определять дефолтные методы в интерфейсах. Этот шаг еще больше размыл различия между абстрактным классом и интерфейсом. Реализуем это в коде: Схема отношений классов без проблемы множественного наследования Проведем рефакторинг в соответствии со схемой. Вынесем из абстрактного класса метод Добавим также классу Таким образом, видно, что класс может расширять только один другой класс, но имплементировать множество интерфейсов. https://gist.github.com/denkarz/InheirenceExample – пример кода *** Тема данной статьи достаточно обширна, и в ней остался не раскрыт ряд вопросов: применение интерфейсов маркеров или примесей, однако я надеюсь, что мне удалось осветить основные аспекты наследования в Java. Подведем итог. В этой статье мы:Что такое наследование?
Пример наследования
Pixel
, Snake
, Food
, Bonus
.
public class Pixel { <...> } class Snake extends Pixel { <...> } class Food extends Pixel { <...> } class Bonus extends Food { <...> }
extends
– ключевое слово, применяемое к классам для указания родительской сущности. В Java, в отличие от, например, C++ родитель может быть только один. Pixel
, были описаны и все остальные классы. То есть классы Snake
, Food
и Bonus
унаследовали все поля и методы класса Pixel
.public abstract boolean canBeEaten();
Pixel
, ведь пиксель обязательно должен быть змеей, едой или бонусом. Следовательно, класс Pixel
– имеет смысл сделать абстрактным классом.public abstract class Pixel
canBeEaten()
в дочерних классах:
class Snake extends Pixel { @Override boolean canBeEaten() { return false; } } class Food extends Pixel { <...> @Override boolean canBeEaten() { return true; } }
Bonus
не переопределяется метод canBeEaten()
. Он наследует свое поведение уже от своего родителя – класса Food
.Проблема множественного наследования
Enemy
вызывает метод canBeEaten()
, определенный в классе Pixel
и при этом не переопределенный в классе Enemy
, а классы Snake
и Food
определили этот метод каждый по-своему. Возникает вопрос: от какого класса должен наследовать свое поведение экземпляр класса Enemy
: Snake
или Food
? Такая конфигурация наследования называется «проблемой множественного наследования».Что такое интерфейс?
Cloneable
из пакета java.lang
.Отличия абстрактного класса от интерфейса
Пример интерфейса
interface Movable { void move(String direction); } class Snake extends Pixel implements Movable { <..> @Override public void move(String direction) { switch (direction) { case "up" -> y = y + 1; case "down" -> y = y - 1; case "left" -> x = x - 1; case "right" -> x = x + 1; } } }
Вредные советы. Делаем из интерфейса абстрактный класс
Mixins
).
interface Messageble { default void infoMessage() { System.out.println("Змейка вырастет"); } } class Food extends Pixel implements Messageble { protected boolean eat() { this.infoMessage(); return true; } }
Решение проблемы множественного наследования
canBeEaten()
в отдельный интерфейс и имплементируем его в абстрактном классе. Таким образом, мы реализовали контракт о том, что все классы дочерние от абстрактного обязаны у себя реализовать метод canBeEaten()
что решает проблему выбора родителя для неопределенного метода и, как следствие, саму проблему ромба.
interface Eatable { boolean canBeEaten(); } abstract class Pixel implements Eatable { <...> }
Snake
интерфейс-маркер, чтобы отличать «живые» объекты. Например, змеек и, возможно, других персонажей от еды.
interface Alivable{} class Snake extends Pixel implements Movable, Alivable { <...> }
Ссылки
Материалы по теме
- 5 views
- 0 Comment