Как в терминологии java принято называть родительский класс

Как в терминологии java принято называть родительский класс

Страница не найдена

Этот вопрос был удалён с сайта Stack Overflow на русском в процессе модерации . Чтобы узнать возможные причины удаления вопроса, обратитесь в справочный центр.

  • JavaScript — найти ключи, зная их значения
  • Эффект подпрыгивания при достижении начала или конца списка RecyclerView
  • Tree — это класс или интерфейс?
  • Зачем делать родительский класс абстрактным?
  • Переопределение private в дочернем классе (PHP)
  • JRE это исполнительная система или это среда выполнения?
  • Получить ссылку на родительский класс
  • Scanner в каждом классе — рационально ли?
  • Что называют представлениями в Android?

Если вы думаете, что здесь чего-то не хватает, свяжитесь с нами.

Нажимая «Принять все файлы cookie», вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.

Урок 8 — Объектно-ориентированное программирование в Java

В этом уроке мы познакомимся с объектно-ориентированным программированием (ООП) на языке Java. Начиная с простейшего проекта в нашей программе встречались классы, но мы пользовались ими несознательно. Сейчас мы познакомимся с терминологией и основными синтаксическими конструкциями ООП. Более подробно, хоть и без привязки к языку Java, тема ООП рассмотрена в нашем учебнике — в частности, из него вы можете узнать как использовать ООП правильно, но чтобы понять материал учебника вам нужно освоить этот урок.
Исходный код примеров и проект для среды Eclipse можно взять в репозитории.

1 Терминология ООП

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

Следующее важное понятие — Объект (экземпляр класса), он соответствует переменной, типом которой является ваш Класс. То есть переменную типа Int — тоже можно назвать объектом. Для нашего примера объектами будут являться Жучка, Дружок и другие экземпляры Собак .

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

  • наследование — механизм позволяющий описать новый класс на основе уже существующего. Новый класс называют дочерним классом, потомком, подклассом или производным классом по отношению к родительскому или базовому классу. Множественное наследование не допускается;
  • полиморфизм — механизм позволяющий изменить реализацию методов, т.е. поведение объекта в дочерних классах;
  • абстракция — возможность объявить метод, без его конкретной реализации. Подобные классы называются абстрактными;
  • инкапсуляция — сокрытие реализации класса от его пользователя. Другими словами, пользователь имеет доступ только к открытым членам класса. К закрытым членам имеет доступ только сам объект;
  • интерфейсы — особый вид абстрактных классов. Они не могут иметь изменяемых членов данных. Класс может реализовывать сколь угодно много интерфейсов. Таким образом, интерфейс формирует поведенческую линию для объектов, не принадлежащих одной иерархии классов.

2 Определение класса

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

Рассмотрим пример простого класса:

Запустите этот простой пример у себя. Дальше мы размеремся с конструкциями, которые вам сейчас не понятны. Наш класс Dog имеет 3 метода, два из которых являются конструкторами, разберемся с ними.

3 Конструкторы класса и другие методы

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

Dog druzhok = new Dog("druzhok");

Члены класса (поля и методы) могут иметь следующие права доступа:

  1. public — доступны кому угодно, как в нашем случае — конструкторы класса и метод speak , которые и вызываются из Main , то есть достпны ему;
  2. protected — доступны только классам наследникам (рассматриваются дальше подробнее), если мы установим такой спецификатор для метода speak — то вызвать его из Main будет нельзя, но если мы создадим класс наследник от Dog — то ему этот метод будет доступен;
  3. private — доступны только внутри класса;
  4. пакетный уровень доступа — элемент считается с пакетным уровнем доступа, если не указан ни один из модификаторов доступа. В таком случае элемент доступен классу в котором объявлен и другим классам в том же пакете, но не доступен классам, в том числе и наследникам, находящимся в других пакетах. Таким образом данный уровень видимости является более строгим чем protected.

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

Методы могут возвращать любой правильный тип, или ничего не возвращать (тогда возвращаемый тип описывается как void — например, у нашего speak() ). Конструкторы же не имеют возвращаемого типа, они не могут возвращать даже тип void .

Конструкторы и методы используют ключевое слово this совершенно по-разному. Метод использует this чтобы получить ссылку на экземпляр класса выполняющего этот метод. Конструкторы используют this чтобы сослаться на другой конструктор в этом же классе, но с другим списком параметров. Если констурктор использует ключевое слово this , то оно должно быть в первой строке, игнорирование этого правила приведет к ошибке компилятора. Переписать наши конструкторы можно так:

Статические методы не используют this ,т.к. они не принадлежат экземпляру класса, поэтому this некуда ссылаться. Статические методы принадлежат классу как целому, но никак не экземпляру класса.

4 Наследование и полиморфизм

Наследование — механизм расширения ваших классов. Допустим, помимо нашего класса Dog , нам потребовались ездовые собаки ( RidingDog ), обладающие скоростью и выносливостью (нужны дополнительные поля). Мы можем реализовать это по разному:

  1. Создать новый класс RidingDog , поместив в него кличку, громкость и все другие поля и методы класса Dog, а затем добавить туда speed и strength . При этом объем кода сильно растет, а его нужно поддерживать. Мало того, когда мы скопировали исходный код класса Dog , то мы скопировали и ошибки, которые в нем могли быть. В чуть более сложных проектах — это совсем не жизнеспособное решение.
  2. Добавить в класс Dog новые поля и методы, но оставить их незаполненными для существующих собак, а для ездовых — заполнять. Это тоже плохое решение, так как приводит к путанице. Завтра нам понадобится охотничья собака, а потом — собака поводырь или кинологическая собака. В лучшем случае — мы получим неуправляемый класс Dog , в худшем — у одной собаки функция find должна работать так, а у другой — иначе.
  3. Использовать механизм наследования, который предназначен как раз для таких случаев и решает проблемы двух предыдущих пунктов. Между классами существуют отношения, так вот наследование — задает отношение «является». Кинологическая собака является собакой — поэтому тут можно и нужно использовать наследование.

Создадим класс RidingDog (в файле RidingDog.java ):

В класс Main добавим еще одну собаку:

В консоль будет дважды выведено "gaw gaw" .

Мы создали класс-наследник, как отмечалось выше, ему будут доступны поля базового класса ( Dog ), помечены как protected . Для этого тут использованы два новых ключевых слова:

  1. extends задает родительский класс новому создаваемому классу. У каждого класса может быть только один базовый — множественное наследование запрещено. По умолчанию любой класс наследуется от класса Object из пакета java.lang , то есть у нашего Dog тоже есть базовый класс.
  2. super используется для инициализации родительского класса — в этом случае вызов родительского конструктора должен быть первой строкой в дочернем конструкторе (как в нашем случае). В более общем случае задает ссылку на родительский класс, чтобы понять это — добавьте в класс RidingDog следующий метод:

Последний пример демонстрирует еще одну важнейшую особенность классов — возможность определить новое поведение методов с таким же именем в классах-наследниках, при этом это поведение может основываться на поведении базового класса — как раз за счет использования super . Аннотация @Override указывает, что далее мы собираемся переопределять метод базового класса. При этом, если в базовом классе не окажется метода с аналогичной сигнатурой, то мы получим предупреждение компилятора о том, что хотя мы и собирались что-то переопределить, по факту этого не произошло.

Венцом всего этого является полиморфизм. В самом деле, вдруг нам потребовалось хранить всех собак (и обычных, и беговых) в одном массиве, но ведь у них разные типы данных, что делать? Как создать такой массив? — Как демонстрирует, приведенный ниже пример, мы без проблем можем создать объект с типом базового класса, а фактически создать на его месте объект класса-наследника (даже если этот объект является элементом массива). Мало того, при вызове у этого объекта метода, переопределенного в классе-наследнике, будет вызван метод наследника:

Возможность работы с клаасами наследниками через набор открытых методов базового класса и называется полиморфизмом.

5 Интерфейсы в Java

Как отмечалось выше, у класса в Java может быть только один класс-наследник, однако часто нужно больше, ведь наследование отражает отношение «является», однако иногда хочется это нарушить — на помощь приходят интерфейсы, так как множественное наследование интерфейсов разрешено.

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

В файле GameItem.java опишем этот интерфейс, примерно также как раньше поступали с классом, но не требуется указывать спецификатор доступа для методов (они всегда публичные):

Наследование от такого интерфейса обязывает класс реализовать метод take_horn . В интерфейсе могут также содержаться поля, они всегда являются константными. Наследование от интерфейса задается ключевым словом implements — например implements A, B, C . Реализовать интерфейс GameItem в классе Dog можно так:

Иногда вам может встретиться пустой интерфейс (т.е. не содержащий ни единого метода) — их используют в качестве пометок для определения, поддерживает ли класс какие-либо возможности. Например, классы с интерфейсом Link имеют разный внутренний доступ к элементам: произвольный ( ArrayList ) или последовательный ( LinkedList ). Чтобы дать возможность понять какой доступ используется, классы с произвольным доступом реализуют дополнительно пустой интерфейс RandomAccess .

Примеры стандартных интерфейсов Java:

  • Cloneable — интерфейс метка, указывает, что вызов метода clone класса Object легален;
  • Comparable/Comparator — интерфейс для сравнения объектов изнутри/из вне;
  • Formatable — для представления объекта в виде форматированного текста;
  • Iterator, Iterable — интерфейс для обхода элементов и интерфейс как обходить элементы;
  • Runnable/Callable — описание задачи потока в виде процедуры/функции;
  • Serializable — для платформонезавимой записи/чтения объекта из потока ввода/вывода;

6 Атрибуты final и static

  1. у членов данных означает, что их значение запрещено менять или другими словами определяет константы. Если финальный член данных не инициализирован при определении, то его значение необходимо указать в конструкторах. Такая альтернатива переменным членам данных может ускорить работу приложения.
  2. можно применить ко всему классу для запрещения наследования от него. Такой класс называется завершенным (например, класс String ).
  3. у методов означает отмену их виртуальности (полиморфизма) — возможности переопределения в дочерних классах. Использование этого атрибута может ускорить работу приложения (как inline в С++). Объекты у которых все члены данных имеют атрибут final называются завершенными (пример, все тот же String

Атрибут static указывает, что член является общим для всех экземпляров класса. Естественно статические методы могут использовать только статические члены. Доступ к статическим членам происходит через имя класса.

Статические финальные члены данных часто используются для эмуляции перечислений.

7 Пакеты Java

Пакеты являются своего рода областью имен и служат для группировки классов. Если класс на диске определяется файлом, то пакет директорией.

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

Если класс определяется с атрибутом public, то он будет доступен для других пакетов.

Для использования классов из других пакетов необходимо указать полный путь к классу,
при этом вместо слешей точка.

Наиболее частые классы можно импортировать ключевым словом import. Импорт располагается до определения класса.

Если необходимо импортировать все классы пакета, то вместо имени класса указывается звездочка. import javax.swing.* Продвинутые среды как Eclipse автоматически добавляют строку импорта для используемого класса.

8 Код генерируемый компилятором

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

public class Example

это эквивалентно написанию:

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

это будет идентично написанию:

Наблюдательный новичок может быть удивлен тому, как вышеописанная программа может вызывать конструктор предка, когда TestConstructor не наследует ни одного класса. Ответом является то, что Java наследует класс Object в том случае, когда явно не указан класс предок. Компилятор автоматически добавляет конструктор без аргументов, если явно не описан конструктор, и автоматически дополняет вызовом super без аргументов, в случае отсутствия явного вызова super . Таким образом, следующие два примера являются функционально эквивалентными:

public class Example

9 Практика наследования

Ниже рассмотрен пример очень простого класса — тысячи таких пишут начинающие программисты. Я рекомендую прочитать рассуждения по поводу этого класса и не повторять ошибок.

Рассмотрите определение класса Point . Какие проблемы могут возникнуть при создании производного класса? Как можно избавиться от этих проблем?

Решение:
На рисунке приведен исходный код класса точка, при этом:

  1. поля x, y являются публичными;
  2. класс не имеет закрытых методов;
  3. интерфейс класса (набор открытых методов) позволяет:
    1. создать точку (Point),
    2. скопировать ее (getLocation),
    3. переместить (задать новые координаты — move, setLocation).

    Явные проблемы — дублирование кода:

    1. методы setLocation(int, int) и move выполняют одно и тоже. Один из этих методов стоит удалить — скорее всего move (название менее удачное).
    2. Метод setLocation(int, int) и setLocation(Point) выполняют одно и тоже. Один из них стоит выразить через другой. Например:

    Кроме этой проблемы, есть ряд других, но не таких очевидных (о вариантах исправления можно рассуждать):

    1. Возможно, стоит изменить имя класса — использовать Location2D вместо Point . Тогда вместо методов setLocation, getLocation можно будет использовать get и set .

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

    2.1 закрыть поля x, y . Тогда задание и изменений их значений будет происходить только через интерфейс класса (открытые методы);
    2.2 удалить все функции getLocation, setLocation, x(), y(), а к полям обращаться явно:

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

    Очевидно, что первый вариант выглядит лучше. Кроме того, сам класс Точки в этом случае элементарный. Работать такой код будет быстрее (так как не будет издержек на вызов функции). Так зачем эти методы? Что они дают?:

    а) Класс должен задавать абстракцию за счет сокрытия деталей реализации. Однако, метод getLocation(int, int) раскрывает эту абстракцию — человек, пользующийся нашим классом все равно знает как класс устроен внутри, несмотря на то, что мы закрыли поля x, y.

    б) Методы могут выполнять дополнительную работу — например, логгирование или валидацию данных. Однако, в данном кокретном случае сложно себе представить такую конструкцию.

    в) Методы, в отличии от полей можно переопределить в классах-наследниках. Для нашего примера — это могла бы быть ЦветнаяТочка или Position3D , например. Однако, цветная точка, скорее всего должна была бы дополнительно возвращать свеой цвет, ей нет смысла переопределяеть методы Location . Трехмерная точка тоже не должна переопределять методы setLocation(int, int). Мало того, эти методы будут ей мешать. Очевидно ли что делает следующий код? — думаю нет:

    Исходя из всего этого, класс точки наиболее правильно реализовать так:

    Тут не нужны ни классы, ни наследование. Наследование трехмерной точки от двухмерной, возможно (в зависимости от предметной области) нарушает принцип постановки Лисков. Является ли трехмерная точка двухмерной? — Вопрос философский, а исходный код — нет. Можно посмотреть на такой пример:

    Окажется, что она является Point2D(1,2) , но почему не Point2D(2,3) или Point2D(1,3) . Ведь убрав одно измерение мы из исходной точки получим не 1 вариант, а 3 — тогда почему программа работает именно так? — Для случаев, чуть сложнее точки ответ на этот вопрос будет еще менее очевиден.

    Методы объектов Java

    В стандартном классе Object определен ряд специфических методов, которые становятся доступны если вы от Object наследуетесь. Не сразу понятно зачем они нужны и какова цена их использования, разбираемся…

    Метод clone() Java

    При вызове метода Java-машина создает и возвращает дубликат объекта, у которого вызвали данный метод. Клонирование объекта в классе Object реализовано очень примитивно – при клонировании создается еще один объект и его полям присваиваются значения полей объекта-образца. Если копируемый объект содержит ссылки на другие объекты, то ссылки будут скопированы, дубликаты тех объектов не создаются.

    Java-машина не знает, какие объекты можно клонировать, а какие нет, к примеру, файлы клонировать нельзя. Поэтому ответственность о полноценном клонировании полностью ложится на плечи разработчика. Тут все было сделано по аналогии с методом equals() , имеется также своеобразный аналог метода hashCode() – это интерфейс Cloneable .

    Интерфейс Cloneable – это интерфейс-маркер. Данный интерфейс не содержит никаких методов. Он используется, чтобы маркировать (помечать) некоторые классы. Если разработчик класса считает, что объекты класса можно клонировать, то он помечает класс этим интерфейсом (имплементирует класс от интерфейса Cloneable ).

    Если разработчика не устраивает стандартная реализация метода clone(), он должен написать свою, которая будет создавать дубликат объекта правильным образом. Для этого нам нужно переопределить данный метод.

    При вызове метода clone(), Java-машина проверяет, имплементирован ли у объекта интерфейс Cloneable . Если данный интерфейс был имплементирован, то клонирует объект методом clone() , если нет — выкидывает исключение CloneNotSupportedException .

    Методы equals() и hashCode

    Прежде чем пытаться понять методы equals() и hashCode() , необходимо вспомнить несколько фактов: в Java при сравнении ссылочных переменных сравниваются не сами объекты, а ссылки на объекты, и что все объекты унаследованы от класса Object , который содержит реализацию методов equals() и hashCode() по умолчанию.

    Для решения задачи сравнения ссылочных переменных существует стандартное решение – метод equals() . Цель данного метода – определить идентичны ли объекты внутри, сравнив их внутреннее содержание. У класса Object есть своя реализация метода equals , которая просто сравнивает ссылки:

    Порой такой реализации бывает не достаточно, поэтому, при необходимости чтобы разные объекты с одинаковым содержимым рассматривались как равные, надо переопределить метод equals() учитывая поля, которые должны участвовать в сравнении объектов. Ведь только разработчик класса знает, какие данные важны, что учитывать при сравнении, а что – нет.

    У метода equals() есть большой минус – он слишком медленно работает. Для этого был придуман метод hashCode(). Для каждого объекта данный метод возвращает определенное число. Какое именно – это тоже решает разработчик класса, как и в случае с методом equals().

    Вместо того чтобы сравнивать объекты, будем сравнивать их hashCode , и только если hashCode-ы равны, сравнивать объекты посредством equals() .

    Разработчик, который реализует функцию hashCode() , должен помнить следующее:

    1) у двух разных объектов может быть одинаковый hashCode;

    2) у одинаковых объектов (с точки зрения equals()) должен быть одинаковый hashCode;

    3) хеш-коды должны быть выбраны таким образом, чтобы не было большого количества различных объектов с одинаковыми hashCode . Ситуация, когда у различных объектов хеш-коды совпадают называется коллизией.

    Важное замечание: при переопределении метода equals(), обязательно нужно переопределить метод hashCode() , с учетом трех вышеописанных правил (Переопределил equals — переопредели и hashCode ).

    Дело в том, что коллекции в Java перед тем как сравнить объекты с помощью equals всегда ищут/сравнивают их с помощью метода hashCode() . И если у одинаковых объектов будут разные hashCode , то объекты будут считаться разными — до сравнения с помощью equals() просто не дойдет.

    Другие материалы

    Более подробно по некоторым разделам статьи можно прочитать на следующих страницах:

    Как в терминологии java принято называть родительский класс

    • Open with Desktop
    • View raw
    • Copy raw contents Copy raw contents

    Copy raw contents

    Copy raw contents

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

    Сперва давайте постараемся ответить на вопрос: Зачем вообще придумали Наследование ?

    Для ответа на него давайте объявим некоторый класс.

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

    Пока все прекрасно и удобно.

    Но представим, что теперь понадобилось создать новый класс Employee — работника.

    Employee имеет те же черты, что и Person , но вдобавок к этому он еще устроен на работу, получает зарплату и т.д. Т.е это тот же Person , но с еще одним дополнительным полем — работа.

    В мире, где механизм наследования отсутствует, мы бы объявили этот класс следующим образом:

    По сути, все что мы сделали — это скопировали один код класса Person и вставили его в Employee , добавив, разумеется, сверху еще щепотку нового функционала. Не мне вам объяснять, что подобный подход, копирования и вставки, это пагубная, опасная и гневающая богов привычка.

    Вдобавок к этому есть и еще один неприятный момент!

    Для нас очевидно, что эти классы связаны логически, т.е Employee — это какой-то Person , устроенный на работу, но для Java эта связь совсем не очевидна, с точки зрения языка это просто два разных, никак не связанных и не имеющих ничего общего класса.

    Но мы-то знаем, что это не так!

    Возникает резонное желание — сделать так, чтобы язык знал о связи наших классов и убрать копирование кода. Разумно, что хочется делегировать ответственность за это копирование кода и поддержание его в консистентном состоянии нашему языку программирования?

    Под консистентным состоянием я имею в виду то, что при добавлении нового поля или метода в Person это поле также появится и у Employee .

    Создание иерархии классов

    Для того, чтобы воспользоваться механизмом наследования, т.е унаследовать один класс от другого, в Java существует ключевое слово extends .

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

    Перепишем теперь наш код с использованием наследования .

    Стоит заметить, что мы изменили модификаторы доступа к полям классов. Модификаторы доступа позволяют вам влиять на то, какие поля и методы классов будут участвовать в наследовании, а какие нет.

    Про модификаторы доступа нужно почитать тут.

    Про this и super.

    Теперь, мало того, что код получился более компактным, так как мы убрали дублирование, так еще и выстроилась иерархия классов. Классы Person и Employee уже больше не являются не связанными, как это было в первом примере.

    Для демонстрации этого мы просто создадим метод, который ждет на вход экземпляры класса Person . Допустим, у нас реализована некая телефонная книга:

    Запустите следующий код c классами, которы мы только что написали:

    Благодаря тому, что Employee является наследником Person код скомпилируется, а при его выполнении вы увидите в консоли:

    Это возможно потому, что производный класс полностью удовлетворяет спецификации родительского. Именно поэтому мы безболезненно в телефонную книгу можем передать и класс Person , и класс Employee .

    Если же вы попробуете повторить тот же трюк, но с классами не использовавшими наследование, как в начальном варианте, код даже не скомпилируется, ведь телефонная книга ждет класс Person . Но Person и Employee в первом варианте — это два абсолютно разных класса, которые никак не связаны друг с другом.

    Важно понимать, что наследование — это не просто инструмент для избавления от дублирования кода.

    Как мы уже обсуждали во введении в ООП, класс — это совокупность поведения и состояния .

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

    Помните, что наследование — это приобретение и состояния, и поведения класса-родителя.

    Наследование — это приобретение и состояния, и поведения класса-родителя.

    Отношения между классами

    Рассмотрим следующий вопрос. У нас есть классы: Оружие и Солдат . Солдат должен уметь стрелять. Допустимо ли отнаследоваться классом Солдат от Оружия , тем самым расширив наш класс и получить необходимые стрелковые навыки?

    Думаю, нет, так делать нелогично. В таком случае будет приобретено не только поведение стрельбы, но и состояние оружия: марка, скорострельность и т.д. А что делать в случае, когда солдат решит сменить оружие? Понятно, что в приведенном случае один класс не расширяет другой, а скорее содержит часть поведения другого.

    В связи с этим возникает вопрос: когда можно использовать наследование, а когда не надо?

    Снова рассмотрим два класса, что были приведены выше: Оружие и Солдат .

    Для ответа на вопрос: уместно ли здесь применить наследование спросите себя: какое отношение между классами? Можно ли сказать, что потенциальный потомок является тем же, что и родительский класс? Т.е отвечает ли класс Солдат отношению is a по отношению к Оружию ?

    Если ответ ‘да’, является, то наследование вполне применимо в этом случае. Если же на вопрос «является ли класс тем же, что и родитель» ответ отрицательный — то в таком случае использовать наследование не рекомендуется.

    В случае отрицательного ответа на предыдущий вопрос следуют снова спросить себя: отвечает ли класс Солдат отношению has a по отношению к Оружию ? Т.е является ли один объект является составной частью другого?

    Скорее всего ответ будет да и тогда правильнее использовать композицию.

    В нашем случае ответ на вопрос: класс Солдат связан взаимоотношением has a по отношению к Оружию . Это значит, что Оружие будет полем класса Солдат . При этом инициализация поля может быть объявлена по разному: через конструктор или через setter -метод.

    В таком случае, поведение, которое мы хотим добавить Солдату , будет объявлено следующим образом:

    Если же ответ на оба вопроса про has a и is a будет нет , то вы скорее всего что-то делаете не так и строите неправильную абстракцию.

    Вопрос:

    Для проверки возьмем еще два класса: Figure и Rectangle . Определите взаимотношение классов и что будет логичнее использовать: наследование или композицию.

    Ответ:

    Является ли прямоуголник некоей абстрактной фигурой? Скорее всего да. Отношение взаимодействия is a . Значит, логично сделать так, что Rectangle является наследником Figure .

    Теперь чуть подробнее поговорим про has a и композицию.

    Существует несколько видов взаимодействия объектов, объединенных под общим понятием «Has-A Relationship» или «Part Of Relationship». Это отношение означает, что один объект является составной частью другого объекта.

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

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

    Мы не тащим за собой все из родительского объекта, а работаем только с составными частями. Т.е это расширение функционала класса за счет внедрения других объектов.

    Отличия композиции от агрегации

    Композиция — это такой случай, когда составные части класса не могут существовать без существования класса, в который они входят.

    Это как сердце и человек, сердце — это часть человека, но отдельно от него оно не может существовать, не может или его существоание не имеет смысла.

    Агрегация — это такой случай, когда составные части класса могут существовать без существования класса, в который они входят.

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

    Композиция — это частный случай агрегации.

    Часто для простоты говорят просто композиция, не делая различий с агрегацией.

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

    Минусы и плюсы подходов

    • Повторное использование уже существующих и протестированных участков кода.
    • Выстраиваемая иерархия наследников.

    Дочерний класс зависит от изменений в родительском классе, изменив что-то в родительском классе, мы автоматически получаем эти изменения в дочернем.

    Пусть у нас есть своя реализация MyHashMap , так как мы хотим переиспользовать часть того, что есть в HashMap , мы отнаследовались от нее и переопределили метод add(. ) . Все отлично, но теперь, если разработчики HashMap добавят метод addAll(..) , использующий add для добавляения элементов, у нас будет в этом месте дыра, ведь у нас-то своя реализация add , а унаследованный метод addAll будет добавлять элементы ‘по старому’.

    Ошибка в неверной иерархии наследования ведет к большим проблемам в использовании кода в будущем.

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

    В качестве примера посмотрите на следующий код:

    Здесь мы через super обратились к полю родительского класса.

    Тянем все проблемы и ошибки наследованного кода.

    Тяжело выстраивать правильные абстракции.

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

    Для иллюстрации этого приведем следующий пример.

    Ситуация с Бонни: MyHashMap

    Почему при композиции мы избегаем ситуации описанной в примере с MyHashMap ?

    Ответ прост: при композиции мы просто создадим HashMap полем класса и сами пропишем интерфейс нашего класса, вызывая лишь те методы, которые нам нужны. При этом ситуаций, которые описаны выше, просто не возникнет — мы определим метод add , как нам надо и даже если разработчики добавят в HashMap какой-то свой еще один addAll метод — нас это никак не затронет, так как наш класс умеет только то, что мы прописали — и не более.

    Т.е при композиции мы максимально контролируем и знаем поведение нашего класса: ведь мы сами его и написали. Никакие «новые» методы не могут попасть в интерфейс нашего класса — пока мы сами их явно не вызовем и не напишем на них свои обертки.

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

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

    Отношение has a

    • Ситуации на подобие той, что описана выше с MyHashMap исключены.
    • Возможность скрыть проблемы класса-родителя, создав обертку, в которой скроем недостатки API класс-родителя.
    • Легко применять и строить абстракции.
    • Иногда действительно неудобно работать с наследованием и иерархией классов.
    • Если объектов-владельцев достаточно много, то создание и уничтожение вместо одного объекта двух или более может пагубно сказаться на производтельнсоти.

    Наследование — это именно расширение какого-то функционала, в то время как композиция — это включение(внедрение) функционала.

    Существует даже правило:

    Предпочитайте композицию наследованию.

    Снова начнем с вопроса: может ли у класса быть более одного предка?

    java.lang.Object мы не берем в виду, так как он является родительским классом для всех.

    В языках программирования типа C++ , Python и т.д это возможно, такой механизм называется множественным наследованием. В Java так сделать нельзя.

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

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

    Применительно к программированию такие просчеты могут быть фатальными и разрушить всю архитектуру приложения. Частично из-за возможности подобных ошибок в Java просто решили не разрешать множественное наследование.

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

    Наследование — это очень мощный инструмент, позволяющий выстраивать иерархии классов, обеспечить переиспользование кода и бороться с дублированием.

    Однако, насколько этот инструмент мощный, настолько же и опасный.

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

    Чаще задавайтесь вопросом: каким отношением связаны ваши классы?

    Если это отношение is a , то это верный признак того, что здесь будет уместно использование наследования. Если же ваше отношение — это has a , то постарайтесь применить агрегацию/композицию.

    Всегда старайтесь избегать дублирования кода и задумывайтесь о полученной иерархии классов.

    Кофе-брейк #95. Как решить проблему множественного наследования в Java

    Кофе-брейк #95. Как решить проблему множественного наследования в Java - 1

    Источник: FreeCodeCamp Java — один из самых популярных объектно-ориентированных языков программирования, используемых сегодня. Поскольку он не зависит от платформы, вы можете найти Java-приложения на всех типах устройств и в каждой операционной системе. А поскольку Java относительно легко выучить, это один из языков, который осваивают многие программисты. Важная особенность Java, с которой вы должны быть знакомы, — это наследование классов. Наследование позволяет оптимизировать код, облегчая повторное использование классов. Когда вы можете повторно использовать код, который уже был протестирован и отлажен, жизненный цикл разработки программного обеспечения становится короче и менее затратным. Хотя теоретически это простая концепция, кодирование отношений наследования требует внимания к деталям. Особенно в отношении множественного наследования, когда один дочерний класс наследует свойства от нескольких родительских классов. Java отвергает множественные отношения наследования, потому что они создают неоднозначность, но есть несколько способов добиться многих из тех же эффектов, если вы знаете, что делать. В этой статье мы рассмотрим проблемы с множественным наследованием и обсудим альтернативные варианты кодирования на Java.

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

    • Классы — это фундаментальная структура шаблона в объектно-ориентированных языках программирования. Класс определяет общие свойства для группы объектов.
    • Родительский класс : также известный как базовые классы или суперклассы. Родительский класс — это расширяемый класс, который предоставляет функции дочернему классу. Он допускает возможность повторного использования. Определения и функции родительского класса повторно используются при создании дочерних классов.
    • Дочерний класс : более обобщенно называемый подклассом, дочерний класс наследует функции от другого класса. Дочерние классы — это расширенные или производные классы.
    • Наследование : отношения между родительским и дочерним классами.

    Типы наследования ООП

    • Одноуровневое наследование : когда дочерний класс наследует функции от единственного родительского класса.
    • Многоуровневое наследование : это многоуровневая форма одноуровневого наследования. При многоуровневом наследовании дочерний класс также может выступать в качестве родительского класса для других дочерних классов. Отношения между каждым уровнем линейны — никакие ветви не выходят выше, чем при множественном наследовании. В этом случае конечный дочерний класс имеет функции со всех уровней выше.
    • Иерархическое наследование : противоположность множественного наследования. В иерархическом наследовании единственный родительский класс имеет более одного дочернего класса. Таким образом, вместо того, чтобы иметь ветви над ним, он разветвляется внизу.
    • Гибридное наследование : как следует из названия, гибридное наследование представляет собой комбинацию других типов наследования.
    • Множественное наследование : при множественном наследовании дочерний класс имеет более одного родительского класса. Хотя Java и JavaScript не поддерживают множественное наследование, такие языки ООП, как C ++, поддерживают.
    • Многопутевое наследование : гибрид множественного, многоуровневого и иерархического наследования, при многопутевом наследовании дочерний класс наследует свои характеристики и функции от родительского класса и нескольких дочерних классов родительского класса. Поскольку многопутевое наследование основано на множественном наследовании, Java не поддерживает его использование.

    Почему Java не поддерживает множественное наследование

    Кофе-брейк #95. Как решить проблему множественного наследования в Java - 3

    Основная проблема множественного наследования заключается в том, что оно может создавать неоднозначности в дочерних классах. В обзорном техническом документе 1995 года ведущий дизайнер Java Джеймс Гослинг заявил, что проблемы с множественным наследованием были одной из причин создания Java. Сложности, присущие множественному наследованию, наиболее отчетливо видны в проблеме алмаза. В задаче “ромб” родительский класс A имеет два различных дочерних класса B и C; то есть дочерние классы B и C расширяют класс A. Теперь мы создаем новый дочерний класс D, который расширяет как класс B, так и класс C. Обратите внимание, что у нас есть множественное наследование (D расширяет B и C), иерархическое наследование (B и C расширяют A) и многоуровневое наследование (D расширяет A, B и C). В проблеме ромба дочерние классы B и C наследуют метод от родительского класса A. И B, и C переопределяют унаследованный метод. Но новые методы в B и C противоречат друг другу. Окончательный дочерний класс D наследует два независимых и конфликтующих метода от своих нескольких родителей B и C. Неясно, какой метод класса D следует использовать, поэтому возникает двусмысленность. Другие языки программирования ООП реализуют различные методы решения неоднозначности множественного наследования.

Добавить комментарий

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