Лекция 4
Тема: Повторное использование кода. Композиция и наследование. Наследование в Java. Наследование членов суперкласса. Инициализация базового класса. Ключевое слово super. Запрет наследования с помощью ключевого слова final. Класс Object. Сравнение композиции и наследования.
Повторное использование кода
Возможность повторного использования кода принадлежит к числу важнейших преимуществ языков объектно-ориентированного программирования.
В Java, вместо того чтобы создавать новый класс "с чистого листа", вы берете за основу уже существующий класс, который кто-то уже создал и проверил на работоспособность. Существую два пути реализации этой идеи.
композиция (composition) - объекты уже имеющихся классов просто создаются внутри нового класса. Программист просто использует функциональность уже готового кода;
наследование (inheritance) - новый класс создается как специализация уже существующего класса. Взяв существующий класс за основу, вы добавляете к нему свой код без изменения существующего класса.
Композиция - использование функционала одних объектов в составе других объектов. Рассмотрим пример класса FileManager
, в котором определен метод для сохранения текстовых данных в файл. Класс Document
использует функционал класса FileManager
, чтобы сохранить текстовый документ на жесткий диск.
Базовые понятия механизма наследования
Наследование - отношение между классами, в котором один класс повторяет структуру и поведение другого класса (или нескольких других классов).
Реализуется наследование путем создания классов на основе уже существующих. При этом члены класса, на основе которого создается новый класс, с некоторыми оговорками, автоматически включается в новый класс. Кроме этого, в новый класс можно добавлять новые члены.
Класс, на основе которого создается новый класс, называется суперклассом (базовым классом, родительским классом). Новый создаваемый класс называется подклассом (дочерним классом, производным классом, классом-наследником и так далее).
Создание подкласса в Java
Создание подкласса практически не отличается от создания обычного класса, кроме необходимости указать суперкласс, на основе которого создается подкласс. В Java для этого существует ключевое слово extends:
В Java, в отличие от C++, отсутствует множественное наследование, то есть подкласс может создаваться на основе только одного суперкласса.
В Java присутствует многоуровневое наследование: подкласс может быть суперклассом для другого класса. Благодаря этому можно создавать целые цепочки классов, связанные механизмом наследования
Наследование членов суперкласса
Если член класса определен как private
, то при наследовании доступ к нему со стороны подкласса закрыт. Важно понимать, что приватный член суперкласса в подклассе есть, только он закрыт для прямого доступа. К примеру, данный код не скомпилируется
Закрытыми могут быть как поля класса, так и его методы. Если необходимо открыть поля или методы для доступа к ним со стороны подкласса, при объявлении членов суперкласса используют слово protected
либо создают геттеры и сеттеры для доступа к полям. К примеру, данный код скомпилируется и будет работать корректно
Данный пример демонстрирует доступ к полям с помощью геттеров
Инициализация базового класса
Так как в наследовании участвуют два класса, базовый и производный, не сразу понятно, какой же объект получится в результате. Внешне все выглядит так, словно новый класс имеет тот же интерфейс, что и базовый класс, плюс еще несколько дополнительных полей и методов.
Для упрощения можете представить, что когда вы создаете объект производного класса, внутри него якобы "содержится" объект базового класса. Этот объект "выглядит" точно так же, как выглядел бы созданный обычным порядком объект базового класса. Поэтому извне представляется, будто бы в объекте производного класса "упакован" объект базового класса.
Данная аналогия не соответствует тому, как наследование реализуется "внутри" языка Java, но пока будем мыслить таким образом.
Чтобы объект базового класса был правильно инициализирован, при вызове конструктора подкласса, сначала вызывается конструктор базового класса, у которого есть необходимые знания и привилегии для проведения инициализации базового класса.
При использовании конструкторов без параметров, у компилятора не возникает проблем с вызовом таких конструкторов, так как нет нужды передавать аргументы. В этом случае Java автоматически вставляет вызовы конструктора базового класса в конструктор производного класса.
Результат работы такого приложения будет следующим
Как видно из данного примера, цепочка вызовов конструкторов начинается с самого базового класса. Таким образом, подобъект базового класса инициализируется еще до того, как он станет доступным для конструктора производного класса. Даже если конструктор класса Cat
не будет определен, Java сгенерирует конструктор по умолчанию, в котором также будет вызван конструктор базового класса.
Если в классе не определен конструктор без параметров, то вызов конструктора базового класса надо будет оформлять явно. К примеру, такой код вызовет ошибку на этапе компиляции
Для явного вызова конструктора суперкласса используется ключевое слово super. Более подробно мы рассмотрим его ниже, а сейчас приведем пример корректного вызова конструктора суперкласса
Ключевое слово 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
Иногда бывает необходимо запретить наследоваться от какого-то класса либо запретить переопределять метод. В этом случае, в объявлении класса или метода укажите ключевое слово final
Класс Object
В Java определен специальный класс Object
, который является суперклассом для всех классов Java. Иными словами, все классы в языке Java являются подклассами, производными от класса Object
.
В классе Object определены перечисленные ниже методы, которые доступны в любом объекте
Метод
Описание
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 наносек)
Ожидает исполнения другого потока
С некоторыми методами класса Object
мы встретимся позже, а сейчас нам интересен только метод toString()
.
Метод toString()
Метод toString()
призван возвращать строковое представление объекта (список значений полей). Рассмотрим пример
Обратите внимание, что класс Box
не содержит никаких членов, однако, так как Box наследуется от класса Object
, ему доступны методы суперкласса и метод toString()
в частности.
Результатом работы данного примера является следующая строка
Метод toString()
в классе Object
выводит полное название класса и 16-ричное представление хеш-кода объекта.
Чтобы метод toString()
выводил нужную информацию, его необходимо пепреопределить как в следующем примере
Результат будет следующим
Также метод toString()
имеет одну примечательную особенность - его можно явно не вызывать, а просто указывать ссылочную переменную, Java сама вызовет метод toString()
. Например, следующий код
выдаст следующий результат
Как вы видите, результат работы строк 5 и 6 является идентичным.
Сравнение композиции и наследования
И композиция, и наследование позволяют вам помещать подобъекты внутрь вашего нового класса (при композиции это происходит явно, а в наследовании - опосредовано). В чем же разница между ними и когда следует выбирать одно, а когда другое?
Композиция в основном используется, когда в новом классе необходимо задействовать функциональность уже существующего класса, но не его интерфейс. То есть вы встраиваете объект, чтобы использовать его возможности в новом классе, а пользователь класса видит только определенный вами интерфейс, но не замечает встроенных объектов. Для этого внедряемые объекты объявляются со спецификатором private.
При использовании наследования, вы берете уже существующий класс и создаете eго специализированную версию. В основном это значит, что класс общего назначения адаптируется для конкретной задачи.
Last updated
Was this helpful?