Как стать автором
Обновить

Абсолютная загадка наследования в Java

Время на прочтение4 мин
Количество просмотров28K
Автор оригинала: Jevgeni Kabanov
Почему эта загадка абсолютная? По двум причинам:
• Она касается основ языка Java, а не какого-то малоизвестного нюанса API.
• Она расплавила мой мозг, когда я на нее наткнулся.
Если вы хотите проверить себя перед дальнейшим чтением, пройдите этот тест.

Для начала подготовим окружение. У нас будет 3 класса в 2 пакетах. Классы C1 и C2 будут в пакете p1:
package p1;
public class C1 {
 public int m() {return 1;}
}
public class C2 extends C1 {
 public int m() {return 2;}
}


* This source code was highlighted with Source Code Highlighter.
Класс C3 будет в отдельном пакете p2:
package p2;
public class C3 extends p1.C2 {
 public int m() {return 3;}
}


* This source code was highlighted with Source Code Highlighter.
Еще нам понадобится тестовый класс p1.Main с таким методом main:
public static void main(String[] args) {
 C1 c = new p2.C3();
 System.out.println(c.m());
}


* This source code was highlighted with Source Code Highlighter.
Обратите внимание, что мы вызываем метод класса С1 у экземпляра класса C3. Как можно было догадаться, этот пример выведет «3».

Теперь давайте изменим видимость m() во всех трех классах на видимость по умолчанию:
public class C1 {
 /*default*/ int m() {return 1;}
}
public class C2 extends C1 {
 /*default*/ int m() {return 2;}
}
public class C3 extends p1.C2 {
 /*default*/ int m() {return 3;}
}


* This source code was highlighted with Source Code Highlighter.
Теперь выводом будет «2»!
Почему это происходит? Класс Main, который осуществляет вызов, не видит метод m() класса C3, поскольку тот находится в отдельном пакете. В том что касается Main, цепь наследования заканчивается на C2. Но так как C2 находится в том же пакете, то его метод m() переопределяет соответствующий метод C1. Не очень интуитивно, но так уж оно работает.

Теперь давайте попробуем нечто иное: изменим модификатор C3.m() обратно на public. Что получится теперь?
public class C1 {
 /*default*/ int m() {return 1;}
}
public class C2 extends C1 {
 /*default*/ int m() {return 2;}
}
public class C3 extends p1.C2 {
 public int m() {return 3;}
}


* This source code was highlighted with Source Code Highlighter.
Теперь Main видит метод C3.m(). Но как это ни странно, результат, по-прежнему, «2»!
По-видимому, считается, что C3.m() вовсе не переопределяет C2.m(). Можно представлять себе это следующим образом: переопределяющий метод должен иметь доступ к методу, который он переопределяет (через super.m()). Однако в данном случае, у C3.m() нет доступа к своему базовому методу, поскольку тот находится в другом пакете. Поэтому C3 считается частью совершенно иной цепочки наследования, не той, что содержит C1 и C2. Если бы мы вызвали C3.m() непосредственно из Main, то получили бы ожидаемый результат «3».

Теперь давайте взглянем на еще один пример. protected — интересный модификатор видимости. Он ведет себя как модификатор по умолчанию для членов того же пакета и как public для подклассов. Что произойдет, если мы изменим все видимости на protected?
public class C1 {
 protected int m() {return 1;}
}
public class C2 extends C1 {
 protected int m() {return 2;}
}
public class C3 extends p1.C2 {
 protected int m() {return 3;}
}


* This source code was highlighted with Source Code Highlighter.
Я рассуждал следующим образом: так как Main не является подклассом любого из наших классов, то protected, в данном случае, должен вести себя так же, как модификатор по умолчанию и результатом должно быть «2». Однако это не так. Важным моментом является то, что C3.m() имеет доступ к super.m() и, таким образом, на самом деле выводом будет «3».
Лично я, когда впервые столкнулся с этой ситуацией, очень запутался и не мог разобраться, пока не изучил все примеры. Пока что можно сделать вывод, что подкласс является частью цепи наследования, тогда и только тогда, когда вы можете из него обратиться к super.m().

Это предположение тоже неверно. Рассмотрим следующий пример:
public class C1 {
 /*default*/ int m() {return 1;}
}
public class C2 extends C1 {
 /*default*/ int m() {return 2;}
}
public class C3 extends p1.C2 {
 /*default*/ int m() {return 3;}
}
public class C4 extends p2.C3 {
 /*default*/ int m() {return 4;}
}


* This source code was highlighted with Source Code Highlighter.
Заметим, что C4 находится в пакете p1. Теперь изменим код класса Main следующим образом:
public static void main(String[] args) {
 C1 c = new C4();
 System.out.println(c.m());
}


* This source code was highlighted with Source Code Highlighter.
Тогда он выведет «4». Однако super.m() не доступен из C4: если к C4.m() добавить аннотацию @Override, то код не будет компилироваться. В то же время, если мы изменим метод main на
public static void main(String[] args) {
 p2.C3 c = new C4();
 System.out.println(c.m());
}


* This source code was highlighted with Source Code Highlighter.
то результатом снова будет «3». Это означает, что C4.m() переопределяет C2.m() и C1.m(), но не C3.m(). Это также делает ситуацию еще более запутанной, а правильное предположение следующим: метод подкласса переопределяет метод базового класса, тогда и только тогда, когда метод в базовом классе доступен из подкласса. Здесь «базовый класс» относится к любому предку, не обязательно прямому родителю.

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

UPD: используйте аннотацию Override, тогда в подобной ситуации код не скомпилируется.

Теги:
Хабы:
Всего голосов 56: ↑47 и ↓9+38
Комментарии46

Публикации

Истории

Работа

Java разработчик
350 вакансий

Ближайшие события