Шрифт:
Итак, наследники могут объявлять поля с именами, совпадающими с родительскими полями. Такие объявления называют скрывающими. При этом объекты будут содержать оба значения, а компилятор будет каждый раз определять, с каким из них надо работать.
Методы
Рассмотрим случай переопределения (overriding) методов:
class Parent {
public int getValue {
return 0;
}
}
class Child extends Parent {
public int getValue {
return 1;
}
}
И строки, демонстрирующие работу с этими методами:
Child c = new Child;
System.out.println(c.getValue);
Parent p = c;
System.out.println(p.getValue);
Результатом будет:
1
1
Можно видеть, что родительский метод полностью перекрыт, значение 0 никак нельзя получить через ссылку, указывающую на объект класса Child. В этом ключевая особенность полиморфизма – наследники могут изменять родительское поведение, даже если обращение к ним производится по ссылке родительского типа. Напомним, что, хотя старый метод снаружи уже недоступен, внутри класса-наследника к нему все же можно обратиться с помощью super.
Рассмотрим более сложный пример:
class Parent {
public int getValue {
return 0;
}
public void print {
System.out.println(getValue);
}
}
class Child extends Parent {
public int getValue {
return 1;
}
}
Что появится на консоли после выполнения следующих строк?
Parent p = new Child;
p.print;
С помощью ссылки типа Parent вызывается метод print, объявленный в классе Parent. Из этого метода делается обращение к getValue, которое в классе Parent возвращает 0. Но компилятор уже не может предсказать, к динамическому методу какого класса произойдет обращение во время работы программы. Это определяет виртуальная машина на основе объекта, на который указывает ссылка. И раз этот объект порожден от Child, то существует лишь один метод getValue.
Результатом работы примера будет:
1
Данный пример демонстрирует, что переопределение методов должно производиться с осторожностью. Если слишком сильно изменить логику их работы, нарушить принятые соглашения (например, начать возвращать null в качестве значения ссылочного типа, если родительский метод такого не допускал), это может привести к сбоям в работе родительского класса, а значит, объекта наследника. Более того, существуют и некоторые обязательные ограничения.
Вспомним, что заголовок метода состоит из модификаторов, возвращаемого значения, сигнатуры и throws -выражения. Сигнатура (имя и набор аргументов) остается неизменной, если говорить о переопределении. Возвращаемое значение также не может меняться, иначе это приведет к появлению двух разных методов с одинаковыми сигнатурами.
Рассмотрим модификаторы доступа.
class Parent {
protected int getValue {
return 0;
}
}
class Child extends Parent {
/* ??? */ protected int getValue {
return 1;
}
}
Пусть родительский метод был объявлен как protected. Понятно, что метод наследника можно оставить с таким же уровнем доступа, но можно ли его расширить (public), или сузить (доступ по умолчанию)? Несколько строк для проверки:
Parent p = new Child;
p.getValue;
Обращение к методу осуществляется с помощью ссылки типа 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 c = new Child;
System.out.println(c.getValue);
Parent p = c;
System.out.println(p.getValue);
Аналогично случаю со статическими переменными, вспоминаем алгоритм обработки компилятором таких обращений к статическим элементам и получаем, что код эквивалентен следующим строкам: