Возможность повторного использования кода принадлежит к числу важнейших преимуществ языков объектно-ориентированного программирования.
композиция (composition) - объекты уже имеющихся классов просто создаются внутри нового класса. Программист просто использует функциональность уже готового кода;
наследование (inheritance) - новый класс создается как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса.
Композиция - использование функционала одних объектов в составе других объектов. Рассмотрим пример класса FileManager
, в котором определен метод для сохранения текстовых данных в файл. Класс Document
использует функционал класса FileManager
, чтобы сохранить текстовый документ на жесткий диск.
Наследование - отношение между классами, в котором один класс повторяет структуру и поведение другого класса (или нескольких других классов).
Класс, на основе которого создается новый класс, называется суперклассом (базовым классом, родительским классом). Новый создаваемый класс называется подклассом (дочерним классом, производным классом, классом-наследником и так далее).
Создание подкласса практически не отличается от создания обычного класса, кроме необходимости указать суперкласс, на основе которого создается подкласс. В Java для этого существует ключевое слово extends:
В Java, в отличие от C++, отсутствует множественное наследование, то есть подкласс может создаваться на основе только одного суперкласса.
В Java присутствует многоуровневое наследование: подкласс может быть суперклассом для другого класса. Благодаря этому можно создавать целые цепочки классов, связанные механизмом наследования
Если член класса определен как private
, то при наследовании доступ к нему со стороны подкласса закрыт. Важно понимать, что приватный член суперкласса в подклассе есть, только он закрыт для прямого доступа. К примеру, данный код не скомпилируется
Закрытыми могут быть как поля класса, так и его методы. Если необходимо открыть поля или методы для доступа к ним со стороны подкласса, при объявлении членов суперкласса используют слово protected
либо создают геттеры и сеттеры для доступа к полям. К примеру, данный код скомпилируется и будет работать корректно
Данный пример демонстрирует доступ к полям с помощью геттеров
Так как в наследовании участвуют два класса, базовый и производный, не сразу понятно, какой же объект получится в результате. Внешне все выглядит так, словно новый класс имеет тот же интерфейс, что и базовый класс, плюс еще несколько дополнительных полей и методов.
Однако наследование не просто копирует интерфейс базового класса. Когда вы создаете объект производного класса, внутри него содержится подобъект базового класса. Этот подобъект выглядит точно так же, как выглядел бы созданный обычным порядком объект базового класса. Поэтому извне представляется, будто бы в объекте производного класса "упакован" объект базового класса.
Чтобы подобъект базового класса был правильно инициализирован, при вызове конструктора подкласса, сначала вызывается конструктор базового класса, у которого есть необходимые знания и привилегии для проведения инициализации базового класса.
При использовании конструкторов без параметров, у компилятора не возникает проблем с вызовом таких конструкторов, так как нет нужды передавать аргументы. В этом случае Java автоматически вставляет вызовы конструктора базового класса в конструктор производного класса.
Результат работы такого приложения будет следующим
Как видно из данного примера, цепочка вызовов конструкторов начинается с самого базового класса. Таким образом, подобъект базового класса инициализируется еще до того, как он станет доступным для конструктора производного класса. Даже если конструктор класса Cat
не будет определен, Java сгенерирует конструктор по умолчанию, в котором также будет вызван конструктор базового класса.
Если в классе не определен конструктор без параметров, то вызов конструктора базового класса надо будет оформлять явно. К примеру, такой код вызовет ошибку на этапе компиляции
Для явного вызова конструктора суперкласса используется ключевое слово super. Более подробно мы рассмотрим его ниже, а сейчас приведем пример корректного вызова конструктора суперкласса
Как было сказано ранее, наследование в Java реализуется следующим образом - в объект производного класса добавляется скрытый объект базового класса, который и обеспечивает вызов методов суперкласса.
Ключевое слово super
как раз и ссылается на этот скрытый объект суперкласса. Используя это ключевое слово, можно получить доступ к членам суперкласса (если позволяет их модификатор доступа)
Как видно из примера, ключевое слово super
имеет что-то общее с ключевым словом this
.
При использовании механизма наследования возникает проблема с использованием методов суперкласса. Часто метод суперкласса не отражает изменения и нововведения, внесенные в подклассе и вызов таких методов дает некорректную информацию об объекте. Рассмотрим следующий пример
Результат работы такого приложения будет следующим
Как вы уже понимаете, в данной части кода
был вызван метод getInfo()
суперкласса, который выводит информацию только о двух параметрах коробки, тогда как класс Box3D
содержит три параметра. Таким образом, метод getInfo()
для класса Box3D
становится как бы некорректным, неправильным, он выдает неполную информацию об объекте. Как решить эту проблему?
Первый вариант - создать в подклассе Box3D
свой метод для вывода информации
Тогда мы будем вызывать метод get3DInfo()
что даст нам корректный результат
Такой вариант является интуитивно понятным, но все-таки не совсем корректным. Теперь у объекта Box3D
существует аж целых два метода для получения информации об объекте, один из которых является некорректным, что вносит путаницу для нас и для тех программистов, которые будут использовать наш код.
Самым правильным вариантом будет как бы "переписать" метод getInfo(), предоставить версию метода getInfo() для класса Box3D
. Таким образом и класс Box
и класс Box3D
будет иметь метод getInfo()
, просто в классе Box3D
он будет иначе реализован, с учетом появления третьего параметра. Кроме того, объект класса Box3D
теперь будет содержать только один метод для получения информации об объекте и этот метод будет корректно работать.
Для реализации своеобразного "переписывания" метода в подклассе, в Java существует механизм переопределения метода (method overriding).
Воспользуемся механизмом переопределения метода, чтобы корректно решить проблему с классами Box
и Box3D
(в примере для наглядности опущены некоторые члены классов)
Обратите внимание что сигнатуры двух методов getInfo()
полностью совпадают - это обязательное условие для срабатывания механизма переопределения метода. Метод в суперклассе называется переопределенным.
Также обратите внимание на строку 33, где записано @Override
. Такая запись называется аннотацией.
Аннотация - это дополнительное пояснение для компилятора и для различных утилит, которые работают с кодом (анализаторы, генераторы документации и так далее). Указание аннотации не является обязательным, код будет работать и без аннотации, но указание аннотации является одним из важных правил грамотного написания кода.
Результат работы нашего примера будет следующим
Обратите внимание, что и для класса Box
и для класса Box3D
мы вызываем метод с одним и тем же названием и с одной и той же сигнатурой
Но в зависимости от того, для какого класса мы вызываем этот метод, в первом случае отрабатывает метод в классе Box
, а во втором случае - метод в классе Box3D
.
Следует отличать механизм перегрузки метода от механизма переопределения метода.
Метод может быть как перегруженным, так и переопределенным.
Иногда бывает необходимо запретить наследоваться от какого-то класса либо запретить переопределять метод. В этом случае, в объявлении класса или метода укажите ключевое слово final
В Java определен специальный класс Object
, который является суперклассом для всех классов Java. Иными словами, все классы в языке Java являются подклассами, производными от класса Object
.
В классе Object определены перечисленные ниже методы, которые доступны в любом объекте
С некоторыми методами класса Object
мы встретимся позже, а сейчас нам интересен только метод toString()
.
Метод toString()
призван возвращать строковое представление объекта (список значений полей). Рассмотрим пример
Обратите внимание, что класс Box
не содержит никаких членов, однако, так как Box наследуется от класса Object
, ему доступны методы суперкласса и метод toString()
в частности.
Результатом работы данного примера является следующая строка
Метод toString()
в классе Object
выводит полное название класса и 16-ричное представление хеш-кода объекта.
Если вам необходимо вывести значения полей объекта в виде строки, используйте метод toString()
, он для этого и был предназначен, его использование является общепринятым правилом.
Метод getInfo()
в примере выше был использован только в демонстрационных целях!
Чтобы метод toString()
выводил нужную информацию, его необходимо пепреопределить как в следующем примере
Результат будет следующим
Также метод toString()
имеет одну примечательную особенность - его можно явно не вызывать, а просто указывать ссылочную переменную, Java сама вызовет метод toString()
. Например, следующий код
выдаст следующий результат
Как вы видите, результат работы строк 5 и 6 является идентичным.
Отношение обобщения (generalization) в UML изображается при помощи сплошной линии и жирной треугольной стрелки, ведущей от подкласса с суперклассу.
Что обозначает это отношение? В спецификации UML по этому поводу сказано
Обобщение - таксономическое отношение между более общим элементом и более конкретным. Каждый экземпляр конкретного элемента также является непрямым экземпляром обобщенного элемента. Таким образом, конкретизированный элемент косвенно обладает свойствами обобщенного элемента
Является ли отношение обобщения синонимом наследования (inheritance) в объектно-ориентированном проектировании? Ответ зависит от области применения отношения. Для диаграммы концептуальных классов из модели предметной области ответом будет "нет". В этом случае отношение обобщения подразумевает то, что суперкласс является множеством, а подкласс - подмножеством. С другой стороны, на диаграмме классов проектирования это отношение подразумевает объектно-ориентированное наследование свойств подкласса от суперкласса.
Композиция (composition), так же известная как композитная агрегация (composite aggregation), является строго определенным типом связи "целое-часть" и полезна в некоторых моделях. Отношение композиции предполагает, что
экземпляр части (например, Wheel
) в каждый момент времени принадлежит только одному целому предмету (например, Car
);
часть всегда принадлежит целому (пальцы не существуют отдельно от руки);
целое ответственно за создание и удаление своих частей - либо через самостоятельное создание\удаление, либо через взаимодействие с другими объектами. Следствием этих ограничений является то, что при уничтожении композитного объекта его части должны быть либо уничтожены, либо присоединены в другому композитному объекту. Например, если реальная настольная игра "Монополия" уничтожается, то также уничтожаются все ее клетки (с концептуальной точки зрения). Аналогично, если программный объект Car
уничтожается, то уничтожаются и программные объекты Wheel
.
Для обозначения композиции в UML используется закрашенный ромб на линии ассоциации со стороны целого.
Метод
Описание
Object clone()
Создает новый объект, аналогичный клонируемому объекту
boolean equals(Object объект)
Определяет равнозначность объектов
void finalize()
Вызывается перед тем, как неиспользуемый объект будет удален "сборщиком мусора"
Class<?> getClass()
Определяет класс объекта во время выполнения
int hashCode()
Возвращает хеш-код, связанный с вызывающим объектом
void notify()
Возобновляет работу потока, ожидающего уведомления от вызывающего объекта
void notifyAll()
Возобновляет работу всех потоков, ожидающих уведомления от вызывающего объекта
String toString()
Возвращает символьную строку, описывающую объект
void wait()
void wait(long мсек)
void wait(long мсек, int наносек)
Ожидает исполнения другого потока
В данной лабораторной работе задания выполняются с помощью плагина EduTools!
При решении задачи сначала добавьте сигнатуру методов и возвращаемые значения для того, чтобы тесты могли скомпилироваться корректно!
Реализуйте каждую задачу с помощью механизма наследования, а потом - с помощью механизма композиции.
Для каждой реализации нарисуйте UML-диаграмму классов. Вы можете нарисовать одну диаграмму для двух заданий, реализованных с помощью наследования и одну диаграмму для заданий, реализованных с помощью механизма наследования.
Дан класс Point
, который моделирует точку в двумерном пространстве. Класс включает в себя следующие конструкторы и публичные методы:
Создайте класс Point3D
, который расширяет класс Point через наследование. Он должен вести себя как Point
, за исключением того что это должна быть точка в трехмерном пространстве, которая хранит значение координаты Z
.
Вы должны предоставить те же методы, что и суперкласс, а также реализовать дополнительное поведение
Класс Point3D()
должен переопределить требуемые методы, чтобы они работали корректно с учетом третьей координаты. Также класс Point3D
должен вести себя иначе в следующих ситуациях:
при вызове метода setLocation(int x, int y)
, координата z
должна быть выставлена в 0;
при вызове метода toString()
, строка должна выводить три координаты, а не две;
Дан класс GroceryBill
, который моделирует чек и хранит список товаров, который покупает человек в супермаркете. Класс включает в себя следующие конструкторы и публичные методы:
Объект GroceryBill
взаимодействует с объектами класса Item
. Класс Item
включает следующие публичные методы:
К примеру, товар стоит 1.35, а размер скидки 0.25 для постоянных покупателей. Это означает, что постоянный покупатель должен заплатить 1.10. Некоторые товары могут не иметь скидки (размер скидки 0.0). В классе GroceryBill
не предусмотрена логика для учета скидки, то есть учитывается только полная стоимость товара.
Разработайте класс DiscountBill
, который расширяет класс GroceryBill
и добавляет логику для учета скидки для постоянных клиентов. Конструктор класс DiscountBill
должен принимать на вход параметр, который указывает, является ли клиент постоянным.
Класс DiscountBill
должен реализовывать собственную логику метода getTotal()
для постоянных покупателей. Например, если полная сумма равна 80 гривен, а скидка для постоянного клиента составила 20 гривен, метод должен возвращать 60 гривен.
Также, вам необходимо отслеживать количество товаров со скидкой (у которых размер скидки больше 0.0), а также общую скидку, как в гривнах, так и в процентах от суммы в чеке (то есть, насколько в процентах постоянный покупатель заплатил меньше, чем если бы скидки не было).
Помимо переопределенных методов, класс DiscountBill
должен иметь следующие конструкторы и публичные методы:
Если покупатель не является регулярным, класс DiscountBill
должен вести себя как будто общая скидка равна 0 и все товары учтены по их полной стоимости.
метод distanceFromOrigin()
должны учитывать координату z и возвращать расстояние по формуле .
Сигнатура
Описание
public Point()
Создает точку с координатами (0, 0)
public Point(int x, int y)
Создает точку с координатами (x, y)
public void setLocation(int x, int y)
Устанавливает новые координаты точки
public int getX()
Возвращает значение координаты X
public int getY()
Возвращает значение координаты Y
public String toString()
Возвращает строку в виде "(x,y)"
public int distanceFromOrigin()
Возвращает расстояние от начала координат (0, 0) до точки по формуле расстояния Евклида
Сигнатура
Описание
public Point3D()
Создает точку с координатами (0, 0, 0)
public Point3D(int x, int y, int z)
Создает точку с координатами (x, y, z)
public void setLocation(int x, int y, int z)
Устанавливает новые координаты
public int getZ()
Возвращает координату Z
Сигнатура
Описание
public GroceryBill(Employee clerk)
Создает объект GroceryBill
для данного clerk
public void add(Item i)
Добавляет товар в чек
public double getTotal()
Возвращает итоговую стоимость товаров
public void printReceipt()
Распечатывает список товаров
Сигнатура
Описание
public double getPrice()
Возвращает стоимость товара
public double getDiscount()
Возвращает скидку для этого товара
Сигнатура
Описание
public DiscountBill
(Employee clerk, boolean regularCustomer)
Создает объект DiscountBill
для данного clerk
public int getDiscountCount()
Возвращает количество товаров со скидкой
public double getDiscountAmount()
Возвращает общую скидку в гривнах
public double getDiscountPercent()
Возвращает процент скидки для товаров (на сколько процентов покупатель заплатил меньше)