Share This
Связаться со мной
Крути в низ
Categories
//☕ Разбираем на простых примерах: наследование в Java

☕ Разбираем на простых примерах: наследование в Java

Разбираемся в нюансах наследования в языке Java и в том, с какими проблемами можно столкнуться в процессе изучения.

razbiraem na prostyh primerah nasledovanie v java 3ee9f1a - ☕ Разбираем на простых примерах: наследование в Java

Начинающие разработчики часто испытывают трудности в понимании основных парадигм ООП. Статья призвана пролить свет на то, как реализуется наследование в Java. Будут затронуты такие понятия, как родительский и дочерние классы, абстрактные классы, проблема ромба и методы ее решения в Java.

Что такое наследование?

После пары вступительных слов перейдем непосредственно к теме статьи.

Наследование – механизм большинства ОО-языков программирования, призванный реализовывать полиморфизм. Полиморфизм – один из принципов ООП. Практическим следствием этого является структурированность кода и выделение общей функциональности в отдельный класс.

Пример наследования

Рассмотрим механизм наследования на примере языка программирования Java.

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

razbiraem na prostyh primerah nasledovanie v java a576267 - ☕ Разбираем на простых примерах: наследование в Java

Начальная схема отношения классов

Исходя из схемы выше, реализуем 4 класса: 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();

abstract – ключевое слово, применяемое к классам и методам. При этом если в классе есть хотя бы один абстрактный метод, то и сам класс должен быть абстрактным. Важно также помнить, что нельзя создавать экземпляры абстрактных классов. Абстрактный метод – метод без реализации.

Так как появился абстрактный метод в неабстрактном классе, нужно разрешить конфликт: определить поведение метода, либо сделать класс абстрактным. Исходя из сформулированной выше постановки задачи, можно сделать вывод, что не может существовать объекта класса 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.

Проблема множественного наследования

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

razbiraem na prostyh primerah nasledovanie v java c333495 - ☕ Разбираем на простых примерах: наследование в Java

Иллюстрация проблемы множественного наследования

Получаем дилемму: если объект класса Enemy вызывает метод canBeEaten(), определенный в классе Pixel и при этом не переопределенный в классе Enemy, а классы Snake и Food определили этот метод каждый по-своему. Возникает вопрос: от какого класса должен наследовать свое поведение экземпляр класса Enemy: Snake или Food? Такая конфигурация наследования называется «проблемой множественного наследования».

В Java для решения этой проблемы применяются интерфейсы.

Что такое интерфейс?

Интерфейс – это объект языка Java схожий по своей сути с абстрактным классом. Для определения интерфейса в языке есть отдельное ключевое слово interface.

Интерфейс не обязан иметь в себе сигнатуры методов. Пустые интерфейсы называются интерфейсами-маркерами. Классическим примером интерфейса-маркера можно считать Cloneable из пакета java.lang.

Отличия абстрактного класса от интерфейса

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

  1. Идеологическая. В парадигме ООП все классы – существительные. Абстрактный класс не исключение. Следовательно, он описывает общие свойства объектов. Интерфейс же — это контракт. Он описывает общие действия доступные всем классам, реализующим его. Интерфейс не располагает реализацией – он лишь гарантирует наличие действия у объекта. Именно поэтому в некоторых нотациях принято именовать интерфейсы как Noun-able.
  2. Практическая. Класс может расширять только один класс и имплементировать множество интерфейсов. С помощью интерфейса можно решить проблему множественного наследования.

Пример интерфейса

Создадим интерфейс, определяющий возможность перемещаться, и имплементируем его:

         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).

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

Реализуем это в коде:

         interface Messageble {    default void infoMessage() {        System.out.println("Змейка вырастет");    } } class Food extends Pixel implements Messageble {    protected boolean eat() {        this.infoMessage();        return true;    } }     

Решение проблемы множественного наследования

razbiraem na prostyh primerah nasledovanie v java c8e5a8d - ☕ Разбираем на простых примерах: наследование в Java

Схема отношений классов без проблемы множественного наследования

Проведем рефакторинг в соответствии со схемой. Вынесем из абстрактного класса метод canBeEaten() в отдельный интерфейс и имплементируем его в абстрактном классе. Таким образом, мы реализовали контракт о том, что все классы дочерние от абстрактного обязаны у себя реализовать метод canBeEaten()что решает проблему выбора родителя для неопределенного метода и, как следствие, саму проблему ромба.

         interface Eatable {    boolean canBeEaten(); } abstract class Pixel implements Eatable { <...> }     

Добавим также классу Snake интерфейс-маркер, чтобы отличать «живые» объекты. Например, змеек и, возможно, других персонажей от еды.

         interface Alivable{} class Snake extends Pixel implements Movable, Alivable {   	<...> }      

Таким образом, видно, что класс может расширять только один другой класс, но имплементировать множество интерфейсов.

​​Ссылки

https://gist.github.com/denkarz/InheirenceExample – пример кода

***

Тема данной статьи достаточно обширна, и в ней остался не раскрыт ряд вопросов: применение интерфейсов маркеров или примесей, однако я надеюсь, что мне удалось осветить основные аспекты наследования в Java.

Подведем итог. В этой статье мы:

  1. Научились создавать интерфейсы, абстрактные классы и методы.
  2. Поняли, чем отличается абстрактный класс от интерфейса.
  3. Познакомились с проблемой множественного наследования, а также методами ее решения.

Материалы по теме

  • ☕ ТОП-20 бесплатных учебных курсов по Java для новичков
  • 👨‍🎓️ Стань востребованным Java-разработчиком вместе с Kata Academy
  • ☕ Разбираемся, почему в Java утекает память несмотря на сборщик мусора

  • 6 views
  • 0 Comment

Leave a Reply

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Связаться со мной
Close