какое ключевое слово запрещает наследование java
Наследование в Java: основные правила с примерами
Наследование в Java — механизм, который позволяет одному классу получить все элементы и свойства другого класса. Этот процесс также называют расширением, преобразованием или отношением «родитель-потомок».
Суперкласс (родитель) — основной класс, от которого принимаются все элементы.
Подкласс (расширенный, дочерний) — класс, который преобразовывается от другого класса, при этом может иметь свои элементы.
Зачем применяют наследование в Java? Суть в том, что можно образовать новые классы, используя уже имеющийся код. При этом есть возможность добавить другие переменные в дочерний класс, получив при этом новый.
Синтаксис:
Ключевое слово extends означает, что создается новый класс от уже имеющегося.
Из примера видно, что класс Apple создается из Fruit, при этом наследуются все его элементы. Так, Fruit — это класс-родитель, а Apple — класс-потомок.
Как наследовать?
Основной и дочерний класcы создают определенную иерархию. На вершине — всегда базовый, а под ним строятся подклассы. При этом преобразованный класс служит родителем для других подклассов.
В Java существуют такие способы преобразования:
Рассмотрим их по порядку.
Одиночное наследование
Механизм одиночного наследования очень прост: подкласс получает свойства только от одного основного класса:
Многоуровневое наследование
Многоуровневый процесс происходит при наследовании дополнительного класса от базового, затем этот же класс действует уже как основной для следующего:
То есть, в конце мы обращаемся к методу walk() от Pet, потом к sleep() от Cat, а затем к purr () от Kitty.
Иерархическое наследование
Иерархическое наследование происходит, когда несколько подклассов получают разрешение от одного суперкласса:
Класс Kitty унаследовал от Pet walk(), а также у него есть свой purr (), соответственно метод sleep() от Cat ему не доступен.
Правила наследования
Написание кода подчиняется своим законам. В Java также есть определенные правила.
Обращение к методу родителя с помощью super
Чтобы обратиться к методу родителя, нужно использовать ключевое слово super.
Давайте используем один из предыдущих вариантов-примеров, добавив super в преобразованный Cow:
В главном Pet есть walk (), также и в расширенном Сow, но уже с приставкой super.walk (), мы обращаемся к walk () в суперкласс из подкласса.
Получение доступа к защищенным элементам и методам
Если protected (защищен) стоит перед элементами и методами — это указание на то, что к ним можно обратиться из производного класса:
Как видно из примера, у нас есть основной класс — Pet — с защищенными name и display. Чтобы обойти эту защиту, мы создали новый объект small подкласса Kitty и получили необходимый доступ.
Наследовать можно всего один класс
Интересно, что в Java нет многократного наследования — оно возможно только от одного суперкласса.
Важно! В Java класс не может унаследовать ничего сам от себя.
Наследуется все, кроме приватных переменных и методов
В Java модификатор private сообщает о том, что классы-наследники не получат от родителя элементы с таким обозначением. Если вы попробуете произвести extends, компилятор выдаст ошибку.
Как переделать метод класса-родителя
Override (переопределение) — мощный инструмент, который применяется, когда необходимо изменить или переопределить реализацию метода, полученного от суперкласса.
Важные моменты при использовании Override:
Как запретить наследование
В результате мы получим ошибку.
Правила наследования в Java
Что такое наследование?
Пример
У всех овчарок, бульдогов и болонок тоже будет четыре лапы и хвост, они тоже будут гавкать и вилять хвостом.
Конечно, мы можем просто брать и копировать эти методы и переменные в каждый класс. Но зачем? Мы можем использовать наследование.
Если мы сделаем все классы пород наследниками класса Dog, они будут иметь доступ ко всем его методам и переменным автоматически. Ну, почти ко всем.
Как наследовать?
Для того, чтобы унаследовать класс, нужно использовать ключевое слово extends:
Какое ключевое слово запрещает наследование java
Основные навыки и понятия
Наследование является одним из трех основополагающих принципов объектно-ориентированного программирования, поскольку оно допускает создание иерархических классификаций. Благодаря наследованию можно создать общий класс, в котором определяются характерные особенности, присущие множеству связанных элементов. От этого класса могут затем наследовать другие, более конкретные классы, добавляя в него свои индивидуальные особенности.
В языке Java наследуемый класс принято называть суперклассом, а наследующий от него класс — подклассом. Следовательно, подкласс — это специализированный вариант суперкласса. Он наследует все переменные и методы, определенные в суперклассе, дополняя их своими элементами.
Наследование одних классов от других отражается в Java при объявлении класса. Для этой цели служит ключевое слово extends. Подкласс дополняет суперкласс, расширяя его.
Рассмотрим простой пример программы, демонстрирующий некоторые свойства наследования. В этой программе определен суперкласс TwoDShape, хранящий сведения о ширине и высоте двумерного объекта. Там же определен и его подкласс Triangle. Обратите внимание на то, что в определении подкласса присутствует ключевое слово extends.
Ниже приведен результат выполнения данной программы.
Здесь в классе TwoDShape определены атрибуты обобщенной двумерной фигуры, конкретным воплощением которой может быть квадрат, треугольник, прямоугольник и т.д. Класс Triangle представляет конкретную разновидность объекта типа TwoDShape, в данном случае — треугольник. Класс Triangle включает в себя все элементы класса TwoDObject, а в дополнение к ним — поле style и методы area() и showStyle(). Описание треугольника хранится в переменной экземпляра style, метод area() вычисляет и возвращает площадь треугольника, а метод showStyle() отображает геометрическую форму треугольника.
В класс Triangle входят все члены суперкласса TwoDShape, и поэтому в теле метода area() доступны переменные экземпляра width и height. Кроме того, с помощью объектов tl и t2 в методе main() можно непосредственно обращаться к переменным width и height, как будто они принадлежат классу Triangle. На рис. 7.1 схематически показано, каким образом суперкласс TwoDShape включается в состав класса Triangle.
Рис. 7.1. Схематическое представление класса Triangle
Несмотря на то что TwoDShape является суперклассом для класса Triangle, он по-прежнему остается независимым классом. Тот факт, что один класс является суперклассом другого класса, совсем не означает, что он не может быть использован самостоятельно. Например, следующий фрагмент кода считается вполне допустимым:
Разумеется, объекту типа TwoDShape ничего не известно о подклассах своего класса TwoDShape, и он не может даже обратиться к ним.
Ниже приведена общая форма объявления класса, наследующего от суперкласса.
Для каждого создаваемого подкласса можно указать только один суперкласс. Множественное наследование в Java не поддерживается, т.е. у подкласса не может быть несколько суперклассов. (Этим Java отличается от языка C++, где можно создать класс, производный сразу от нескольких классов. Об этом не следует забывать, преобразуя код C++ в код Java.) С другой стороны, вполне допустима многоуровневая иерархия, в которой один подкласс является суперклассом другого подкласса. И конечно же, класс не может быть суперклассом для самого себя.
Главное преимущество наследования заключается в следующем: как только будет создан суперкласс, в котором определены общие для множества объектов атрибуты, он может быть использован для создания любого числа более конкретных подклассов. А в каждом подклассе может быть точно выстроена своя собственная классификация. В качестве примера ниже приведен еще один подкласс, производный от суперкласса TwoDShape и инкапсулирующий прямоугольники.
В класс Rectangle входят все члены класса TwoDShape. Кроме того, он содержит метод is Square(), определяющий, является ли прямоугольник квадратом, а также метод area(), вычисляющий площадь прямоугольника.
Доступ к членам класса и наследование
Как пояснялось в главе 6, члены класса зачастую объявляются закрытыми, чтобы исключить их несанкционированное или незаконное использование. Но наследование класса не отменяет ограничения, накладываемые на доступ к закрытым членам класса. Поэтому если в подкласс и входят все члены его суперкласса, то в нем все равно оказываются недоступными те члены суперкласса, которые являются закрытыми. Так, если сделать закрытыми переменные экземпляра width и height в классе TwoDShape, они станут недоступными в классе Triangle, как показано ниже.
Класс Triangle не будет скомпилирован, поскольку ссылки на переменные экземпляра width и height в методе area() нарушают правила доступа. Эти переменные объявлены закрытыми (private), и поэтому они доступны только членам собственного класса. А его подклассам запрещено обращаться к ним.
Напомним, что член класса, объявленный закрытым (private), недоступен за пределами своего класса. Это ограничение распространяется и на подклассы.
На первый взгляд, ограничение на доступ к закрытым членам суперкласса из подкласса кажется трудно преодолимым, поскольку оно не дает во многих случаях возможности пользоваться закрытыми членами этого класса. Но на самом деле это не так. Как пояснялось в главе 6, для обращения к закрытым членам класса в программах на Java обычно используются специальные методы доступа. Ниже в качестве примера приведены видоизмененные классы TwoDShape и Triangle, в которых обращение к переменным экземпляра width и height осуществляется с помощью специальных методов доступа.
Конструкторы и наследование
В иерархии классов допускается, чтобы у суперклассов и подклассов были свои собственные конструкторы. В связи с этим возникает следующий резонный вопрос: какой конструктор отвечает за построение объекта подкласса: конструктор суперкласса, конструктор подкласса или же оба вместе? На этот вопрос можно ответить так: конструктор суперкласса конструирует родительскую часть объекта, а конструктор подкласса — производную часть этого объекта. И в этом есть своя логика, поскольку суперклассу неизвестны и недоступны любые элементы подкласса, а следовательно, их конструирование должно происходить раздельно. В приведенных выше примерах данный вопрос не возникал, поскольку они опирались на автоматическое создание конструкторов, используемых в Java по умолчанию. Но на практике конструкторы определяются явным образом в большинстве классов. Ниже будет показано, каким образом разрешается подобная ситуация.
Если конструктор определен только в подклассе, то все происходит очень просто: конструируется объект подкласса, а родительская часть объекта автоматически конструируется конструктором суперкласса, используемым по умолчанию. В качестве примера ниже приведен переработанный вариант класса Triangle, в котором определяется конструктор, а член style этого класса делается закрытым, так как теперь он устанавливается конструктором.
Здесь конструктор класса Triangle, помимо поля style, инициализирует также унаследованные члены класса TwoDClass.
Если конструкторы объявлены как в подклассе, так и в суперклассе, то дело несколько усложнятся, поскольку должны быть выполнены оба конструктора. В таком случае на помощь приходит ключевое слово super, доступное в двух общих формах. С помощью первой формы вызывается конструктор суперкласса. А вторая форма служит для доступа к членам суперкласса, скрываемым членами подкласса. Рассмотрим первое применение ключевого слова super.
Применение ключевого слова super для вызова конструктора суперкласса
Для вызова конструктора суперкласса служит следующая общая форма ключевого слова super:
где список_параметров обозначает параметры, необходимые для нормальной работы конструктора суперкласса. Вызов конструктора super() должен быть первым оператором в теле конструктора подкласса. Для того чтобы лучше понять особенности вызова super(), рассмотрим вариант класса TwoDShape из следующего примера программы, где определен конструктор, инициализирующий переменные экземпляра width и height:
В конструкторе Triangle присутствует вызов конструктора super() с параметрами w и h. В результате управление получает конструктор TwoDShape(), инициализирующий переменные width и height значениями, передаваемыми ему в качестве параметров. Теперь класс Triangle уже не занимается инициализацией элементов суперкласса. Он должен инициализировать только собственную переменную экземпляра style. Конструктору TwoDShape() предоставляется возможность построить соответствующий подобъект так, как требуется для данного класса. Более того, в суперклассе TwoDShape можно реализовать функции, о которых не будут знать его подклассы. Благодаря этому код становится более устойчивым к ошибкам.
Любая форма конструктора, определенного в суперклассе, может быть вызвана с помощью оператора super(). Для выполнения выбирается тот вариант конструктора, который соответствует указываемым аргументам. В качестве примера ниже приведена расширенная версия классов TwoDShape и Triangle, содержащих конструкторы по умолчанию и конструкторы, принимающие один или более аргумент.
Выполнение этой версии программы дает следующий результат:
Еще раз напомним основные свойства вызова конструктора super(). Когда этот вызов присутствует в конструкторе подкласса, происходит обращение к конструктору его непосредственного суперкласса. Таким образом, вызывается конструктор того класса, который непосредственно породил вызывающий класс. Это справедливо и при многоуровневой иерархии. Кроме того, вызов конструктора super() должен быть первым оператором в теле конструктора подкласса.
Применение ключевого слова super для доступа к членам суперкласса
Существует еще одна общая форма ключевого слова super, которая применяется подобно ключевому слову this, но ссылается на суперкласс данного класса. Эта общая форма обращения к члену суперкласса имеет следующий вид:
где член_класса обозначает метод или переменную экземпляра.
Данная форма ключевого слова super применяется в тех случаях, если член подкласса скрывает член суперкласса. Рассмотрим следующий пример несложной иерархии классов:
Результат выполнения данной программы выглядит следующим образом:
Несмотря на то что переменная экземпляра i в классе В скрывает одноименную переменную в классе А, ключевое слово super позволяет обращаться к переменной i из суперкласса. Аналогичным образом ключевое слово super можно использовать для вызова методов суперкласса, скрываемых методами подкласса.
Пример для опробования 7.1. Расширение класса Vehicle
Для того чтобы продемонстрировать возможности наследования, расширим класс Vehicle, созданный в главе 4. Напомним, что класс Vehicle инкапсулирует данные о транспортных средствах и, в частности, сведения о количестве пассажиров, объеме топливного бака и потреблении топлива. Воспользуемся классом Vehicle в качестве заготовки для создания более специализированных классов. Например, транспортным средством, помимо прочих, является грузовик. Одной из важных характеристик грузовика является его грузоподъемность. Поэтому для создания класса Truck можно расширить класс Vehicle, добавив переменную экземпляра, хранящую сведения о допустимом весе перевозимого груза. В этом проекте переменные экземпляра будут объявлены в классе Vehicle как закрытые (private), а для обращения к ним будут созданы специальные методы доступа.
Создание многоуровневой иерархии классов
В представленных до сих пор примерах программ использовались простые иерархии классов, состоявшие только из суперкласса и подкласса. Но в Java можно также строить иерархии, состоящие из любого числа уровней наследования. Как упоминалось выше, многоуровневая иерархия идеально подходит для использования одного подкласса в качестве суперкласса для другого подкласса. Так, если имеются три класса, А, в и С, то класс С может наследовать от класса В, а тот, в свою очередь, от класса А. В таком случае каждый подкласс наследует характерные особенности всех своих суперклассов. В частности, класс С наследует все члены классов В и А.
Для того чтобы стало понятнее назначение многоуровневой иерархии, рассмотрим следующий пример программы. В этой программе подкласс Triangle выступает в роли суперкласса для класса ColorTriangle. Класс ColorTriangle наследует все свойства классов Triangle и TwoDShape, а также содержит поле color, определяющее цвет треугольника.
Результат выполнения данной программы выглядит следующим образом:
Благодаря наследованию в классе ColorTriangle можно использовать ранее определенные классы Triangle и TwoDShape, дополняя их лишь данными, необходимыми для конкретного применения класса ColorTriangle. Таким образом, наследование способствует повторному использованию кода.
Данный пример демонстрирует еще одну важную деталь: оператор super() всегда обращается к конструктору ближайшего суперкласса. Иными словами, оператор super() в классе ColorTriangle означает вызов конструктора класса Triangle, а в классе Triangle — вызов конструктора класса TwoDShape. Если в иерархии классов для конструктора суперкласса предусмотрены параметры, то все суперклассы должны передавать их вверх по иерархической структуре. Это правило действует независимого от того, нужны ли параметры самому подклассу или не нужны.
Порядок вызова конструкторов
В связи с изложенным выше в отношении наследования и иерархии классов может возникнуть следующий резонный вопрос: когда создается объект подкласса и какой конструктор выполняется первым: тот, что определен в подклассе, или же тот, что определен в суперклассе? Так, если имеется суперкласс А и подкласс В, то вызывается ли конструктор класса А раньше конструктора класса В, или же наоборот? Ответ на этот вопрос состоит в том, что в иерархии классов конструкторы вызываются по порядку выведения классов: от суперкласса к подклассу. Более того, оператор super() должен быть первым в конструкторе подкласса, и поэтому порядок, в котором вызываются конструкторы, остается неизменным, независимо от того, используется ли оператор super() или нет. Если оператор super() отсутствует, то выполняется конструктор каждого суперкласса по умолчанию (т.е. конструктор без параметров). В следующем примере программы демонстрируется порядок вызова конструкторов:
Ниже приведен результат выполнения данной программы.
Как видите, конструкторы вызываются по порядку выведения их классов.
По зрелом размышлении можно прийти к выводу, что вызов конструкторов по порядку выведения их классов имеет определенный смысл. Ведь суперклассу ничего не известно ни об одном из производных от него подклассов, и поэтому любая инициализация, которая требуется его членам, осуществляется совершенно отдельно от инициализации членов подкласса, а возможно, это и необходимое условие. Следовательно, онадолжна выполняться первой.
Ссылки на суперкласс и объекты подклассов
Как вам должно быть уже известно, Java является строго типизированным языком программирования. Помимо стандартных преобразований и автоматического продвижения простых типов данных, в этом языке строго соблюдается принцип совместимости типов. Это означает, что переменная ссылки на объект класса одного типа, как правило, не может ссылаться на объект класса другого типа. В качестве примера рассмотрим следующую простую программу:
Несмотря на то что классы X и Y содержат одинаковые члены, переменной типа X нельзя присвоить ссылку на объект типа Y, поскольку типы объектов отличаются. Вообще говоря, переменная ссылки на объект может указывать только на объекты своего типа.
Но из этого строгого правила соблюдения типов имеется следующее важное исключение: переменной ссылки на объект суперкласса может быть присвоена ссылка на объект любого производного от него подкласса. Следовательно, по ссылке на объект суперкласса можно обращаться к объекту подкласса. Ниже приведен соответствующий пример.
В данном примере класс Y является подклассом X. Следовательно, переменной х2 можно присвоить ссылку на объект типа Y.
Следует особо подчеркнуть, что доступ к конкретным членам класса определяется типом переменной ссылки на объект, а не типом объекта, на который она ссылается. Это означает, что если ссылка на объект подкласса присваивается переменной ссылки на объект суперкласса, то доступ разрешается только к тем частям этого объекта, которые определяются суперклассом. Именно поэтому переменной х2 недоступен член b класса Y, когда она ссылается на объект этого класса. И в этом есть своя логика, поскольку суперклассу ничего не известно о тех членах, которые добавлены в производный от него подкласс. Именно поэтому последняя строка кода в приведенном выше примере была закомментирована.
Несмотря на кажущийся несколько отвлеченным характер приведенных выше рассуждений, им можно найти ряд важных применений на практике. Одно из них рассматривается ниже, а другое — далее в этой главе, когда речь пойдет о переопределении методов.
Один из самых важных моментов для присваивания ссылок на объекты подкласса переменным суперкласса наступает тогда, когда конструкторы вызываются в иерархии классов. Как вам должно быть уже известно, в классе нередко определяется конструктор, принимающий объект своего класса в качестве параметра. Благодаря этому в классе может быть сконструирована копия его объекта. Этой особенностью можно выгодно воспользоваться в подклассах, производных от такого класса. В качестве примера рассмотрим очередные версии классов TwoDShape и Triangle. В оба класса добавлены конструкторы, принимающие объект своего класса в качестве параметра.
В приведенном выше примере программы объект t2 конструируется на основании объекта tl, и поэтому они идентичны. Результат выполнения данной программы выглядит следующим образом:
Обратите внимание на конструктор класса Triangle, код которого приведен ниже.
В качестве параметра данному конструктору передается объект Triangle, который затем с помощью вызова super() передается конструктору TwoDShape, как показано ниже.
Следует заметить, что конструктор TwoDshape() должен получить объект типа TwoDShape, но конструктор Triangle() передает ему объект типа Triangle. Тем не менее никаких недоразумений не возникает. Ведь, как пояснялось ранее, переменная ссылки на суперкласс может ссылаться на объект производного от него подкласса. Следовательно, конструктору TwoDShape() можно передать ссылку на экземпляр подкласса, производного от класса TwoDShape. Конструктор TwoDShape() инициализирует лишь те части передаваемого ему объекта подкласса, которые являются членами класса TwoDShape, и поэтому не имеет значения, содержит ли этот объект дополнительные члены, добавленные в производных подклассах.
В иерархии классов часто присутствуют методы с одинаковой сигнатурой и одинаковым возвращаемым значением как в суперклассе, так и в подклассе. В этом случае говорят, что метод суперкласса переопределяется в подклассе. Если переопределяемый метод вызывается из подкласса, то всегда выбирается тот вариант метода, который определен в подклассе. А вариант метода, определенный в суперклассе, скрывается. Рассмотрим в качестве примера следующий фрагмент кода:
Выполнение этого фрагмента кода дает следующий результат:
Когда метод show() вызывается для объекта типа В, выбирается вариант этого метода, определенный в классе В. Таким образом, вариант метода show() в классе В переопределяет вариант одноименного метода, объявленный в классе А.
Если требуется обратиться к исходному варианту переопределяемого метода, т.е. тому, который определен в суперклассе, следует воспользоваться ключевым словом super. Например, в приведенном ниже варианте класса В из метода show() вызывается вариант того же метода, определенный в суперклассе. При этом отображаются все переменные экземпляра.
Если подставить новый вариант метода show() в предыдущую версию рассматриваемого здесь фрагмента кода, результат его выполнения изменится и будет иметь следующий вид:
В данном случае super. show() — это вызов метода show(), определенного в суперклассе.
Переопределение метода происходит только в том случае, если сигнатуры переопределяемого и переопределяющего методов совпадают. В противном случае происходит обычная перегрузка методов. Рассмотрим следующую видоизмененную версию предыдущего примера:
Выполнение этого фрагмента кода дает следующий результат:
На этот раз в варианте метода show() из класса В предусмотрен строковый параметр. И благодаря этому сигнатура данного метода отличается от сигнатуры метода show() из класса А, для которого параметры не предусмотрены. В результате переопределение метода не происходит.
в переопределяемых методах Примеры из предыдущего раздела демонстрируют переопределение методов, но по ним трудно судить, насколько богатые возможности предоставляет этот механизм. В самом деле, если переопределение методов используется только для соблюдения соглашений о пространствах имен, то его можно рассматривать как любопытную, но почти бесполезную особенность языка программирования. Но это совсем не так. Переопределение методов лежит в основе одного из наиболее эффективных языковых средств Java: динамической диспетчеризации методов, представляющей собой механизм вызова переопределяемых методов, когда выбор конкретного метода осуществляется не на этапе компиляции, а в процессе выполнения программы. Динамическая диспетчеризация методов имеет очень большое значение, поскольку именно с ее помощью принцип полиморфизма реализуется на стадии выполнения программ на Java.
Прежде всего напомним один очень важный принцип: переменная ссылки на суперкласс может ссылаться на объект подкласса. В Java этот принцип используется для вызова переопределяемых методов во время выполнения. Если переопределяемый метод вызывается по ссылке на суперкласс, то исполняющая система Java определяет по типу объекта, какой именно вариант метода следует вызвать, причем делает это во время выполнения программы. Если ссылки указывают на разные типы объектов, то и вызываться будут разные версии переопределенных методов. Иными словами, вариант переопределенного метода для вызова определяет не тип переменной, а тип объекта, на который она ссылается. Так, если суперкласс содержит метод, переопределяемый в подклассе, то вызывается метод, соответствующий тому типу объекта, на который указывает переменная ссылки на суперкласс.
Ниже приведен простой пример, демонстрирующий динамическую диспетчеризацию методов в действии.
Результат выполнения данной программы выглядит следующим образом:
В данном примере программы определяются суперкласс Sup и два его подкласса Subl и Sub2. В классе Sup объявляется метод who(), переопределяемый в его подклассах. В методе main() создаются объекты типа Sup, Subl и Sub2. Там же объявляется переменная supRef ссылки на объект типа Sup. Затем переменной supRef в методе main() поочередно присваиваются ссылки на объекты разного типа, и далее эти ссылки используются для вызова метода who(). Как следует из результата выполнения данной программы, вызываемый вариант метода who() определяется типом объекта, на который ссылается переменная supRef в момент вызова, а не типом самой этой переменной.
Причины для переопределения методов
Как упоминалось выше, переопределяемые методы обеспечивают соблюдение принципа полиморфизма при выполнении программ на Java. Полиморфизм в объектно-ориентированных программах имеет большое значение потому, что благодаря ему появляется возможность объявить в суперклассе методы, общие для всех его подклассов, а в самих подклассах — определить специфические реализации всех этих методов или некоторых из них. Переопределение методов — один из способов, которыми в Java реализуется принцип полиморфизма “один интерфейс — множество методов”.
Залогом успешного применения полиморфизма является, в частности, понимание того, что суперклассы и подклассы образуют иерархию в направлении от меньшей специализации к большей. Если суперкласс организован правильно, он предоставляет своему подклассу все элементы, которыми тот может пользоваться непосредственно. В нем также определяются те методы, которые должны быть по-своему реализованы в порожденных классах. Таким образом, подклассы получают достаточную свободу определять собственные методы, реализуя в то же время согласованный интерфейс. Сочетая наследование с переопределением методов, в суперклассе можно определить общую форму методов для использования во всех его подклассах.
Демонстрация механизма переопределения методов на примере класса TwoDShape Для того чтобы стало понятнее, насколько эффективным является механизм переопределения методов, продемонстрируем его на примере класса TwoDShape. В приведенных ранее примерах в каждом классе, порожденном от класса TwoDShape, определялся метод area(). Теперь мы знаем, что в этом случае имеет смысл включить метод area() в состав класса TwoDShape и позволить каждому его подклассу переопределить этот метод: в частности, реализовать вычисление площади в зависимости от конкретного типа геометрической фигуры. Именно такой подход и воплощен в приведенном ниже примере программы. Для удобства в класс TwoDShape добавлено поле name, упрощающее написание демонстрационных программ.
Ниже приведен результат выполнения данной программы.
Рассмотрим код данной программы более подробно. Теперь, как и предполагалось при написании программы, метод area() входит в состав класса TwoDShape и переопределяется в классах Triangle и Rectangle. В классе TwoDShape метод area() играет роль заполнителя и лишь уведомляет пользователя о том, что этот метод должен быть переопределен в подклассе. При каждом переопределении метода area() в нем реализуются средства, необходимые для того типа объекта, который инкапсулируется в подклассе. Так, если требуется реализовать класс для эллипсов, метод area() придется переопределить таким образом, чтобы он вычислял площадь этой фигуры.
У рассматриваемой здесь программы имеется еще одна важная особенность. Обратите внимание на то, что в методе main() геометрические фигуры объявляются как массив объектов типа TwoDShape. Но на самом деле элементами массива являются ссылки на объекты Triangle, Rectangle и TwoDShape. Это вполне допустимо. Ведь, как пояснялось ранее, переменная ссылки на суперкласс может ссылаться на объект его подкласса. В этой программе организован перебор элементов массива в цикле и вывод сведений о каждом объекте. Несмотря на всю простоту данного примера, он наглядно демонстрирует потенциальные возможности как наследования, так и переопределения методов. Тип объекта, на который указывает переменная ссылки на суперкласс, определяется на этапе выполнения программы и обрабатывается соответствующим образом. Если объект является производным от класса TwoDShape, его площадь вычисляется при вызове метода area(). Интерфейс для данной операции оказывается общим и не зависит от того, с какой именно геометрической фигурой приходится иметь дело каждый раз.
Применение абстрактных классов
Иногда требуется создать суперкласс, в котором определяется лишь самая общая форма для всех его подклассов, а наполнение ее деталями предоставляется каждому из этих подклассов. В таком классе определяется лишь характер методов, которые должны быть конкретно реализованы в подклассах, а не в самом суперклассе. Подобная ситуация возникает, например, в связи с невозможностью получить содержательную реализацию метода в суперклассе. Именно такая ситуация была продемонстрирована в варианте класса TwoDShape из предыдущего примера, где метод area() был просто определен как заполнитель. Такой метод не вычисляет и не выводит площадь двумерной геометрической формы любого типа.
Создавая собственные библиотеки классов, вы можете сами убедиться в том, что у метода зачастую отсутствует содержательное определение в контексте его суперкласса. Подобное затруднение разрешается двумя способами. Один из них, как показано в предыдущем примере, состоит в том, чтобы просто выдать предупреждающее сообщение. И хотя такой способ может пригодиться в некоторых случаях, например при отладке, в практике программирования он обычно не применяется. Ведь в суперклассе могут быть объявлены методы, которые должны быть переопределены в подклассе, чтобы этот класс стал содержательным. Рассмотрим для примера класс Triangle. Он был бы неполным, если бы в нем не был переопределен метод area(). В подобных случаях требуется какой-то способ, гарантирующий, что в подклассе действительно будут переопределены все необходимые методы. И такой способ в Java имеется. Он состоит в использовании абстрактного метода.
Абстрактный метод создается с помощью указываемого модификатора типа abstract. У абстрактного метода отсутствует тело, и поэтому он не реализуется в суперклассе. Это означает, что он должен быть переопределен в подклассе, поскольку его вариант из суперкласса просто непригоден для использования. Для определения абстрактного метода служит приведенная ниже общая форма,
Как видите, у абстрактного метода отсутствует тело. Модификатор abstract может применяться только к обычным методам, но не к статическим методам (static) и конструкторам.
Класс, содержащий один или более абстрактный метод, должен быть также объявлен как абстрактный, для чего перед его объявлением class указывается модификатор abstract. А поскольку реализация абстрактного класса не определяется полностью, то у него не может быть объектов. Следовательно, попытка создать объект абстрактного класса с помощью оператора new приведет к ошибке во время компиляции.
Когда подкласс наследует абстрактный класс, в нем должны быть реализованы все абстрактные методы суперкласса. В противном случае подкласс должен быть также определен как abstract. Таким образом, атрибут abstract наследуется до тех пор, пока не будет достигнута полная реализация класса.
Используя абстрактный класс, мы можем усовершенствовать рассматривавшийся ранее класс TwoDShape-Для неопределенной двумерной геометрической фигуры понятие площади не имеет никакого смысла, поэтому в приведенной ниже версии предыдущей программы метод area() и сам класс TwoDShape объявляются как abstract. Это, конечно, означает, что во всех классах, производных от класса TwoDShape, должен быть переопределен метод area().
Как показывает представленный выше пример программы, во всех подклассах, производных от класса TwoDShape, метод area() должен быть непременно переопределен. Убедитесь в этом сами, попробовав создать подкласс, в котором не переопределен метод area(). В итоге вы получите сообщение об ошибке во время компиляции. Конечно, возможность создавать ссылки на объекты типа TwoDShape по-прежнему существует, и это было сделано в приведенном выше примере программы, но объявлять объекты типа TwoDShape уже нельзя. Именно поэтому массив shapes сокращен в методе main() до 4 элементов, а объект типа TwoDShape для общей двумерной геометрической формы больше не создается.
И еще одно, последнее замечание. Обратите внимание на то, что в классе TwoDShape по-прежнему присутствуют определения методов showDim() и getName() и перед их именами нет модификатора abstract. В абстрактные классы вполне допускается (и часто практикуется) включать конкретные методы, которые могут быть использованы в своем исходном виде в подклассе. А переопределению в подклассах подлежат только те методы, которые объявлены как abstract.
Использование ключевого слова final
Какие бы богатые возможности ни представляли механизмы наследования и переопределения методов, иногда требуется запретить их действие. Допустим, создается класс, в котором инкапсулированы средства управления некоторым устройством. Кроме того, в этом классе пользователю может быть разрешено инициализировать устройство, чтобы воспользоваться некоторой секретной информацией. В таком случае пользователи данного класса не должны иметь возможность переопределять метод инициализации устройства. Для этой цели в Java предоставляется ключевое слово final, позволяющее без труда запретить переопределение метода или наследование класса.
Предотвращение переопределения методов
Для того чтобы предотвратить переопределение метода, в начале его объявления нужно указать модификатор доступа final. Переопределять объявленные подобным образом методы нельзя. Ниже приведен фрагмент кода, демонстрирующий использование ключевого слова final для подобных целей.
Поскольку метод meth() объявлен как final, его нельзя переопределить в классе В. Если вы попытаетесь сделать это, возникнет ошибка при компиляции программы.
Предотвратить наследование класса можно, указав в определении класса ключевое слово final. В этом случае считается, что данное ключевое слово применяется ко всем методам класса. Очевидно, что не имеет никакого смысла применять ключевое слово final к абстрактным классам. Ведь абстрактный класс не завершен по определению, и объявленные в нем методы должны быть реализованы в подклассах.
Ниже приведен пример класса, подклассы которого создавать запрещено.
Как следует из комментариев к данному примеру, недопустимо, чтобы класс В наследовал от класса А, так как последний определен как final.
Применение ключевого слова final к переменным экземпляра
Помимо рассмотренных ранее примеров использования, ключевое слово final можно применять и к переменным экземпляра. Подобным способом создаются именованные константы. Если имени переменной предшествует модификатор final, то значение этой переменной не может быть изменено на протяжении всего времени выполнения программы. Очевидно, что подобным переменным нужно присваивать начальные значения. В главе 6 был рассмотрен простой класс ErrorMsg для обработки ошибок. В нем устанавливается соответствие между кодами ошибок и символьными строками сообщений об ошибках. Ниже приведен усовершенствованный вариант этого класса, в котором для создания именованных констант применяется модификатор final. Теперь, вместо того чтобы передавать методу getErrorMsg() числовое значение, например 2, достаточно указать при его вызове именованную целочисленную константу DISKERR.
Обратите внимание на то, как используются константы в методе main(). Они являются членами класса ErrorMsg, и поэтому для доступа к ним требуется ссылка на объект этого класса. Разумеется, константы могут быть унаследованы подклассами и непосредственно доступными в них.
Многие программирующие на Java пользуются именами констант типа final, составленными полностью из прописных букв, как в предыдущем примере. Но это не строгое правило, а только принятый стиль программирования.
В Java определен специальный класс Object. По умолчанию он считается суперклассом всех остальных классов. Иными словами, все классы являются подклассами, производными от класса Object. Это означает, что переменная ссылки на объект типа Object может ссылаться на объект любого класса. Более того, переменная ссылки на объект типа 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 наносекунды) | Ожидает исполнения другого потока |
Методы getClass(), notify(), notifyAll() и wait() объявлены как final, а остальные методы можно переопределять в подклассах. Некоторые из этих методов будут описаны далее в этой книге. Два из них — equals() и toString() — заслуживают особого внимания. Метод equals() сравнивает два объекта. Если объекты равнозначны, то он возвращает логическое значение true, иначе — логическое значение false. Метод toString() возвращает символьную строку, содержащую описание того объекта, которому принадлежит этот метод. Он автоматически вызывается в том случае, если объект передается методу println() в качестве параметра. Во многих классах этот метод переопределяется. В этом случае описание специально подбирается для конкретных типов объектов, которые в них создаются.
Обратите внимание на необычный синтаксис, описывающий значение, возвращаемое методом getClass(). Это обобщенный тип. С помощью обобщений в Java можно указывать в качестве параметра тип данных, используемый в классе или методе. Более подробно обобщения рассматриваются в главе 13.
Упражнение для самопроверки по материалу главы 7