Программирование на Java
Шрифт:
Поля
Начнем с полей, которые могут быть статическими или динамическими. Рассмотрим пример:
class Parent {
int a=2;
}
class Child extends Parent {
int a=3;
}
Прежде всего, нужно сказать, что такое объявление корректно. Наследники могут объявлять поля с любыми именами, даже совпадающими с родительскими. Затем, необходимо понять, как два одноименных поля будут сосуществовать. Действительно, объекты класса Child будут содержать сразу две переменных, а поскольку они могут отличаться не только значением, но и типом (ведь это два независимых
Child c = new Child;
System.out.println(c.a);
Parent p = c;
System.out.println(p.a);
Обе ссылки указывают на один и тот же объект, порожденный от класса Child, но одна из них имеет такой же тип, а другая – Parent. Отсюда следуют и результаты:
3
2
Объявление поля в классе-наследнике "скрыло" родительское поле. Данное объявление так и называется – "скрывающим" (hiding). Это особый случай перекрытия областей видимости, отличный от "затеняющего" (shadowing) и "заслоняющего" (obscuring) объявлений. Тем не менее, родительское поле продолжает существовать. К нему можно обратиться и явно:
class Child extends Parent {
int a=3;
// скрывающее объявление
int b=((Parent)this).a;
// более громоздкое объявление
int c=super.a;
// более простое
}
Переменные b и c получат значение, хранящееся в родительском поле a. Хотя выражение с super более простое, оно не позволит обратиться на два уровня вверх по дереву наследования. А ведь вполне возможно, что в родительском классе это поле также было скрывающим и в родителе родителя хранится еще одно значение. К нему можно обратиться явным приведением, как это делается для b.
Рассмотрим следующий пример:
class Parent {
int x=0;
public void printX {
System.out.println(x);
}
}
class Child extends Parent {
int x=-1;
}
Каков будет результат следующих строк?
new Child.printX;
Значение какого поля будет распечатано? Метод вызывается с помощью ссылки типа Child, но это не сыграет никакой роли. Вызывается метод, определенный в классе Parent, и компилятор, конечно, расценил обращение к полю x в этом методе именно как к полю класса Parent. Поэтому результатом будет 0.
Перейдем к статическим полям. На самом деле, для них проблем и конфликтов, связанных с полиморфизмом, не существует.
Рассмотрим пример:
class Parent {
static int a=2;
}
class Child extends Parent {
static int a=3;
}
Каков будет результат следующих строк?
Child c = new Child;
System.out.println(c.a);
Parent p = c;
System.out.println(p.a);
Нужно вспомнить, как компилятор обрабатывает обращения к статическим полям через ссылочные значения. Неважно, на какой объект указывает ссылка. Более того, она может быть даже равна null. Все определяется типом ссылки.
Поэтому рассматриваемый пример эквивалентен:
System.out.println(Child.a)
System.out.println(Parent.a)
А
3
2
Можно привести следующее пояснение. Статическое поле принадлежит классу, а не объекту. В результате появление классов-наследников со скрывающими (hiding) объявлениями никак не сказывается на работе с исходным полем. Компилятор всегда может определить, через ссылку какого типа происходит обращение к нему.
Обратите внимание на следующий пример:
class Parent {
static int a;
}
class Child extends Parent {
}
Каков будет результат следующих строк?
Child.a=10;
Parent.a=5;
System.out.println(Child.a);
В этом примере поле a не было скрыто и передалось по наследству классу Child. Однако результат показывает, что это все же одно поле:
5
Несмотря на то, что к полю класса идут обращения через разные классы, переменная всего одна.
Итак, наследники могут объявлять поля с именами, совпадающими с родительскими полями. Такие объявления называют скрывающими. При этом объекты будут содержать оба значения, а компилятор будет каждый раз определять, с каким из них надо работать.
Методы
Рассмотрим случай переопределения (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.