Возможность доступа и переопределение
Поможем в ✍️ написании учебной работы
Поможем с курсовой, контрольной, дипломной, рефератом, отчетом по практике, научно-исследовательской и любой другой работой

Метод может быть переопределён только в том случае, если он доступен. Метод, помеченный как private, не «виден» за пределами класса. Если вдруг в производном классе «объявится» метод с теми же сигнатурой и типом возвращаемого значения, что и метод private базового класса, это будут два совершенно разных и не связанных между собой метода – метод производного класса в этой ситуации нельзя квалифицировать как переопределённый.

Что всё это значит в практическом смысле? Внешний вызов метода производного класса (если предположить, что метод доступен извне) не таит никаких сюрпризов – вызывается именно тот, объявленный в производном классе, метод, который требуется, - такое поведение вполне предсказуемо. Если говорить об объекте базового класса, любой вызов одноимённого приватного метода так же означает буквально то, что предполагается, - программа обращается именно к методу базового класса, а не к одному из возможных его «однофамильцев», принадлежащих производным классам. Коротко говоря, обращение к методу private всегда приводит к вызову метода, объявленного в текущем классе.

Сокрытие статических членов

Статические члены класса не могут быть переопределены, поскольку всегда скрыты – как поля, так и методы. Обращение к каждому статическому полю или методу чаще всего осуществляется посредством задания имени класса, в котором они объявлены. Из факта сокрытия статического поля или метода в результате объявления одноимённого члена производного класса вытекает одно следствие – если для доступа к статическому члену используется ссылка, то, как и в ситуации с сокрытием полей, при выборе подходящей альтернативы компилятор учитывает объявленный тип ссылки, а не тип объекта, на который она указывает.

Служебное слово super

Служебное слово super может быть использовано в теле любого нестатического члена класса. При доступе к полю или вызове метода выражение super действует как ссылка на текущий объект, представленный как экземпляр базового класса. Применение super – это единственный вариант, когда выбор метода обусловливается типом ссылки. Вызов super.method всегда означает обращение к методу method базового класса – никакие возможные переопределённые реализации метода в производных классах в расчёт не принимаются. Пример 26 показывает super в действии:

Пример 26.

class That {

/** возврат строки с именем класса */

protected String nm() {

      return "That";

}

}

class More extends That {

protected String nm() {

      return "More";

}

protected void printNM() {

      That sref = (That) this;

      System.out.println("this.nm() = " + this.nm());

      System.out.println("sref.nm() = " + sref.nm());

      System . out . println (" super . nm () = " + super . nm ());

}

}

Хотя sref и super , как кажется, ссылаются на один и тот же объект типа That , только при использовании super компилятор игнорирует тип текущего объекта, обращаясь к методу nm базового класса. Ссылка sref действует точно так же, как и this , и приказывает компилятору выбирать одну из двух реализаций метода nm , принимая во внимание фактический класс объекта. Ниже приведён результат работы метода printNM .

this . nm () = More

sref . nm () = More

super . nm () = That

Совместимость и преобразование типов

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

Совместимость

В любой ситуации, когда значение выражения присваивается некоторой переменной – в процессе инициализации, в предложении присваивания или при передаче аргумента с последующим неявным присваиванием его параметру метода, – тип выражения должен быть совместим (compatible) с типом переменной. Если говорить о ссылочных типах, это означает, что выражение должно относиться к тому же типу, что и переменная, либо к соответствующему производному типу. Например, любой метод, для которого предусмотрена передача аргумента типа Attr, способен воспринимать и аргумент типа ColorAttr, если ColorAttr является производным классом базового класса Attr. Подобное принято называть совместимостью присваивания (assignment compatibility). Но обратное утверждение не верно: не удастся ни явно присвоить значение типа Attr переменной класса ColorAttr, ни передать аргумент типа Attr методу, в котором объявлен параметр класса ColorAttr.

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

Значение null – это специальный случай: null позволено присваивать переменным всех ссылочных типов, включая и массивы.

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

Явное преобразование типов

Оператор преобразования типов позволяет сообщить компилятору, что конкретное выражение следует трактовать таким образом, будто оно относится к тому типу, который явно указан. Оператор может быть применён в любых ситуациях, но его обычное употребление связано всё-таки с преобразованиями, предусматривающими сужение типа. Оператор преобразования типов представляет собой конструкцию, состоящую из наименования требуемого типа, заключённого в круглые скобки, которую размещают непосредственно перед выражением, подлежащим преобразованию. В примере 26, а именно в теле метода printMN , мы уже использовали выражение преобразования с расширением типа:

That sref = (That) this;

Хотя эта попытка преобразования оказалась бесполезной и безуспешной, мы, тем не менее, точно обозначили свои намерения – необходимость интерпретации текущего объекта в виде экземпляра базового класса. Если бы затем мы захотели присвоить sref обратно некоторой ссылочной переменной mref более узкого типа More , наличие оператора преобразования типов оказалось бы существенным:

More mref = (More) sref;

Даже в том случае, если подлинный тип объекта, «скрывающегося» за ссылкой, нам известен, его всё ещё следует сообщить компилятору, обратившись к оператору преобразования типов.

Преобразование с расширением типа нередко обозначают терминами преобразование вверх ( upcasting ) или безопасное преобразование, поскольку тип, расположенный на низкой ступени иерархии, приводится к классу более высокого уровня, а подобная операция заведомо допустима. Преобразование с сужением типа соответственно называют преобразованием вниз ( downcasting ) – воздействию подвергается объект базового типа, который должен быть интерпретирован как объект производного типа. Преобразование вниз – это ещё и небезопасное преобразование, поскольку его результат в общем случае может быть неверен.

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

Проверка типа

Часто возникает задача определения принадлежности объекта тому или иному типу, и решить её позволяет оператор instanceof, возвращающий в результате вычисления значение true, если выражение левой части совместимо с типом, название которого указано в правой части, и false – в противном случае. Следует иметь в виду, что null нельзя причислить к какому бы то ни было типу, и поэтому результат применения instanceof по отношению к null всегда равен false. Используя instanceof, мы можем загодя убедиться в правомерности преобразования с сужением типа, которое хотим выполнить, и избежать возникновения исключительных ситуаций (пример 27):

Пример 27.

if (sref instanceof More)

mref = ( More ) sref ;

Заметим, что мы всё ещё обязаны применять оператор преобразования типов, чтобы сообщить компилятору о наших истинных намерениях.

Конструкция проверки типов с помощью оператора instanceof особенно полезна, когда методу, как правило, не требуется передавать аргумент более широкого типа, но если подобное всё-таки происходит, метод обязан отреагировать должным образом. Например, некий метод сортировки sort , как предусмотрено в его объявлении, обычно работает с объектом класса List , но если в качестве аргумента передаётся ссылка на объект SortedList , который уже отсортирован, делать далее уже ничего не нужно:

public static void sort(List list) {

if (list instanceof SortedList)

     return; // уже всё готово

else

    // сортировка списка

}

Методы и классы final

Обозначая метод класса модификатором final, мы имеем в виду, что ни один производный класс не в состоянии переопределить этот метод, изменив его внутреннюю реализацию. Другими словами, речь идёт о финальной версии метода. Класс в целом может быть помечен как final:

final class NoExtending {

//…

}

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

Применение признака final в объявлениях классов и методов способно повысить уровень безопасности кода. Если класс снабжен модификатором final , никто не в состоянии расширить класс и, вероятно, нарушить при этом его контракт (не в смысле сигнатур доступных методов, а в смысле функциональности, которая обещана этим классом). Если признаком final обозначен метод, вы можете полностью доверять его внутренней реализации во всех ситуациях, не опасаясь «подделки». Уместно применять final , например, в объявлении метода, предусматривающего проверку пароля, вводимого пользователем, чтобы гарантировать точное исполнение того, что методом предусмотрено изначально. Возможному злоумышленнику не удастся изменить исходную реализацию такого метода, «подсунув» программе его переопределённую версию, которая, скажем, всегда возвращает значение true , свидетельствующее об успешной регистрации пользователя, независимо от того, какой пароль он ввёл на самом деле. Если позволяет конкретная ситуация, можно пойти дальше и объявить как final класс целиком, метод, предусматривающий проверку пароля, приобретёт тоже свойство косвенным путём.

Во многих случаях для достижения достаточного уровня безопасности кода вовсе нет необходимости обозначать весь класс как final – вполне возможно сохранить способность класса к расширению, пометив модификатором final только его «критические» структурные элементы. В этом случае вы оставите в неприкосновенности основные функции класса и одновременно разрешите его наследование с добавлением новых членов, но без переопределения «старых». Разумеется, поля, к которым обращается код методов final, должны быть в свою очередь помечены как final или private, так как в противном случае любой производный класс получит возможность изменить их содержимое, воздействуя на поведение соответствующих методов.

Ещё один эффект применения модификатора final связан с упрощением задачи оптимизации кода, решаемой компилятором. Когда вызывается метод, не помеченный как final, исполняющая система определяет фактический класс объекта, связывает вызов с наиболее подходящим кодом из группы перегруженных методов и передаёт управление этому коду. Но если метод, например, getName, обозначен как final, операция обращения к нему упрощается. В самом простом случае, подобном тому, который касается getName, компилятор может заменить вызов метода кодом его тела. Такой механизм носит название встраивания кода ( inlining ). При использовании inline-версии метода getName два следующих выражения выполняются совершенно одинаково:

System.out.println ("id = " + rose.name);

System.out.println ("id = " + rose.getName());

Та же схема оптимизации может быть применена компилятором и по отношению к методам private и static, так как и они не допускают переопределения.

Использование модификатора final в объявлениях классов способствует также повышению эффективности некоторых операций проверки типов. В этом случае многие подобные операции могут быть выполнены уже на стадии компиляции и поэтому потенциальные ошибки выявляются гораздо раньше. Если компилятор встречает в исходном тексте ссылку на класс final, он может быть «уверен», что соответствующий объект относится именно к тому типу, который указан. Компилятор в состоянии сразу определить место, занимаемое классом в общей иерархии классов, и проверить, верно тот используется или нет. Если модификатор final не применяется, соответствующие проверки осуществляются только на стадии выполнения программы.

Методы и классы abstract

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

Абстрактные классы находят широкое применение в тех случаях, когда, например, некоторые признаки поведения класса приемлемы для всех его объектов, но при этом существуют и такие функциональные особенности, которые целесообразно реализовать только в определённых производных классах, а не в базовом классе непосредственно. Объявления подобных классов снабжают модификатором abstract, тем же признаком помечают и методы класса, в объявлении которого отсутствует блок тела. Если необходимо создать класс, в котором все методы должны быть абстрактными, возможно, имеет смысл воспользоваться объявлением интерфейса, об интерфейсах будет рассказано далее.

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

Любой класс, содержащий методы abstract, сам должен быть обозначен как abstract. Подобная кажущаяся избыточность на самом деле очень полезна – читатель, взглянув на объявление класса, сразу видит его «абстрактный» характер и ему не нужно полностью просматривать текст класса в поисках объявлений абстрактных методов.

Абстрактный метод должен быть переопределён в любом производном классе, если только тот не помечен как abstract.

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

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

Класс Object

Класс Object находится на вершине иерархии классов Java. Object явно или косвенно наследуется всеми классами, поэтому переменная типа Object способна указывать на объект любого типа, будь то экземпляр какого-либо класса или массив. Любой переменной типа Object нельзя непосредственно присваивать значения простых типов (таких как int, boolean и т.п.), но эти ограничения легко обойти, «запаковав» значения в объекты соответствующих классов-оболочек (Integer, Boolean и др.), которые будут описаны далее.

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

Дата: 2019-02-19, просмотров: 265.