Почему эта загадка абсолютная? По двум причинам:
• Она касается основ языка Java, а не какого-то малоизвестного нюанса API.
• Она расплавила мой мозг, когда я на нее наткнулся.
Если вы хотите проверить себя перед дальнейшим чтением, пройдите этот тест.
Для начала подготовим окружение. У нас будет 3 класса в 2 пакетах. Классы
Теперь давайте изменим видимость
Почему это происходит? Класс
Теперь давайте попробуем нечто иное: изменим модификатор
По-видимому, считается, что
Теперь давайте взглянем на еще один пример.
Лично я, когда впервые столкнулся с этой ситуацией, очень запутался и не мог разобраться, пока не изучил все примеры. Пока что можно сделать вывод, что подкласс является частью цепи наследования, тогда и только тогда, когда вы можете из него обратиться к
Это предположение тоже неверно. Рассмотрим следующий пример:
Мораль истории такова: хотя данное поведение и описано в спецификации, оно неинтуитивно. Кроме того, может существовать не одна цепь наследования, а много, и, меняя модификатор видимости с «по-умолчанию» на
UPD: используйте аннотацию Override, тогда в подобной ситуации код не скомпилируется.
• Она касается основ языка 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, тогда в подобной ситуации код не скомпилируется.