Можно привести следующее пояснение. Статическое поле принадлежит классу, а не объекту В результате появление классов-наследников со скрывающими (hiding) объявлениями никак не сказывается на работе с исходным полем. Компилятор всегда может определить, через ссылку какого типа происходит обращение к нему.
В этом примере поле а не было скрыто и передалось по наследству классу Child. Однако результат показывает, что это все же одно поле: 5
Несмотря на то, что к полю класса идут обращения через разные *^ассы, переменная всего одна. Итак, наследники могут объявлять поля с именами, совпадающими ^ родительскими полями. Такие объявления называют скрывающими.
При этом объекты будут содержать оба значения, а компилятор будет каждый раз определять, с каким из них надо работать.
Методы
Рассмотрим случай переопределения (overriding) методов:
class Parent {public intgetValueO { return 0;
} } class Child extends Parent {public int getValueO { return 1;} }
И строки, демонстрирующие работу с этими методами:
Child с = new Chrld();
System.out.println(c.getValue());
Parent p = с;
System.out.println(p.getValue());
Результатом будет: 1
Можно видеть, что родительский метод полностью перекрыт, значение 0 никак нельзя получить через ссылку, указывающую на объект класса Child. В этом ключевая особенность полиморфизма — наследники могут изменять родительское поведение, даже если обращение к ним производится по ссылке родительского типа. Напомним, что, хотя старый метод снаружи уже недоступен, внутри класса-наследника к нему все же можно обратиться с помощью super.
Рассмотрим более сложный пример:
class Parent {
public int getValueO {
return 0; }
public void printO {
System.out. println(getValue()); } }
class Child extends Parent {
public int getValueO { return 1;} }
Что появится на консоли после выполнения следующих строк?
Parent р = new Child(); p.printO;
С помощью ссылки типа Parent вызывается метод print(), объявленный в классе Parent. Из этого метода делается обращение к getValue(), которое в классе Parent возвращает 0. Но компилятор уже не может предсказать, к динамическому методу какого класса произойдет обращение во время работы программы. Это определяет виртуальная машина на основе объекта, на который указывает ссылка. И раз этот объект порожден от Child, то существует лишь один метод getValue().
Результатом работы примера будет: 1
Данный пример демонстрирует, что переопределение методов должно производиться с осторожностью. Если слишком сильно изменить логику их работы, нарушить принятые соглашения (например, начать возвращать null в качестве значения ссылочного типа, если родительский метод такого не допускал), это может привести к сбоям в работе родительского класса, а значит, объекта наследника. Более того, существуют и некоторые обязательные ограничения.
Вспомним, что заголовок метода состоит из модификаторов, возвращаемого значения, сигнатуры и throws-выражения. Сигнатура (Имя и набор аргументов) остается неизменной, если говорить о переопределении. Возвращаемое значение также не может меняться, и начало это приведет к появлению двух разных методов с одинаковыми сигнатурами.
Рассмотрим модификаторы доступа.
class Parent {
protected int getValue() { return 0;
} }
class Child extends Parent {
/* ??? 7 protected Int getValue() { return 1;
} }
Пусть родительский метод был объявлен как protected. Понято, что метод наследника можно оставить с таким же уровнем доступа, но можно ли его расширить (public), или сузить (доступ по умолчанию)? Несколько строк для проверки:
Parent р = new Child(); p.getValueO;
Обращение к методу осуществляется с помощью ссылки типа Parent. Именно компилятор выполняет проверку уровня доступа, и он будет ориентироваться на родительский класс. Но ссылка-то указывает на объект, порожденный от Child, и по правилам полиморфизма исполняться будет метод именно этого класса. А значит, доступ к переопределенному методу не может быть более ограниченным, чем к исходному Итак, методы с доступом по умолчанию можно переопределять с таким же доступом, либо protected или public, Protected-методы переопределяются такими же, или public, а для public менять модификатор доступа и вовсе нельзя.
Что касается private-методов, то они определены только внутри класса, снаружи не видны, а потому наследники могут без ограничений объявлять методы с такими же сигнатурами и произвольными возвращаемыми значениями, модификаторами доступа и т.д.
Аналогичные ограничения накладываются и на throws-выражен не, которое будет рассмотрено в следующих лекциях.
Если абстрактный метод переопределяется неабстрактным, то говорят, что он его реализовал (implements). Как ни странно, абстрактный метод может переопределить другой абстрактный, или даже неабстрактный, метод. В первом случае такое действие может иметь смысл только при изменении модификатора доступа (расширении), либо throws-выражения. Во втором случае полностью утрачивается старая реализация метода, что может потребоваться в особенных случаях.
Перейдем к статическим методам. Рассмотрим пример:
class Parent {
static public int getValue() { return 0;
} }
class Child extends Parent { static public int getValue() { return 1;
} }
И строки, демонстрирующие работу с этими методами:
Child с = new ChildO;
System.out.println(c.getValue());
Табл. 8.1. Взаимосвязь типа переменной и типов ее возможных значений.
Parent p = c;
System.out.println(p.getValue());
Аналогично случаю со статическими переменными, вспоминаем алгоритм обработки компилятором таких обращений к статическим элементам и получаем, что код эквивалентен следующим строкам: