Привет! Представляю вашему вниманию перевод статьи A Curious Java Language Feature and How it Produced a Subtle Bug автора Lukas Eder.
Правила видимости в Java иногда могут быть слегка запутанными и непонятными. Как вы думаете, что будет выведено на экран после запуска этого кода?
package p;
import static p.A.x;
class A {
static String x = "A.x";
}
class B {
String x = "B.x";
}
class C {
String x = "C.x";
class D extends B {
void m() {
System.out.println(x);
}
}
}
public class X {
public static void main(String[] args) {
new C().new D().m();
}
}
Будет выведено:
B.x
Потому, что:
Члены суперкласса B скрывают все вложенные элементы класса C, которые, в свою очередь, перекрывают статический импорт класса A.
Каким же образом все это может привести к ошибкам?
Проблема не в том, что приведенный выше пример кода хитроумно написан сам по себе. Нет. Просто, когда вы пишите согласно такой логике, то все будет работать именно так, как и предполагалось. Но, что случится, если мы что-нибудь изменим? Ну, например, если вы измените модификатор доступа члена суперкласса на private:
package p;
import static p.A.x;
class A {
static String x = "A.x";
}
class B {
private String x = "B.x"; // Здесь изменен модификатор доступа
}
class C {
String x = "C.x";
class D extends B {
void m() {
System.out.println(x);
}
}
}
public class X {
public static void main(String[] args) {
new C().new D().m();
}
}
Теперь, как ни странно, B.x больше не видим для метода m(), в данном случае применяется уже другое правило. А именно:
Вложенные элементы (классы) скрывают статический импорт
И в итоге мы получаем следующий результат:
C.x
Конечно, мы можем снова внести некоторые изменения и получим следующий код:
package p;
import static p.A.x;
class A {
static String x = "A.x";
}
class B {
private String x = "B.x";
}
class C {
String xOld = "C.x"; // Здесь внесены изменеия
class D extends B {
void m() {
System.out.println(x);
}
}
}
public class X {
public static void main(String[] args) {
new C().new D().m();
}
}
Как все мы знаем 50% переменных, которые более не будут использоваться переименовываются и получают приставку “old”.
В этом финальном варианте кода остается единственное возможное значение x в при вызове метода m() и это статически импортированный A.x. Таким образом, вывод будет следующим:
A.x
Тонкости, которые стоит учитывать при работе с большими программными кодами
Данный рефакторинг нам показал, что ограничение видимости существующего элемента достаточно опасно, так как работа подкласса могла бы зависеть от него, но по случайному стечению обстоятельств, в коде оказывается другой, «менее важный» и более видимый член, имеющий такое же имя, который включается в работу и не дает вам получить ошибку компиляции.
То же самое происходит, когда вы расширяете область видимости члена какого-либо класса, который до этого имел модификатор доступа private. Из-за этого он может оказаться в области видимости всех своих подклассов, хотя вы могли и не хотеть этого.
При статическом импорте вы также можете столкнуться с аналогичной проблемой. Когда ваш код зависит от статического импорта, который внезапно оказаться скрытым другим членом, имеющим идентичное имя, например, членом суперкласса. Автор вносимых изменений может этого даже не заметить потому, что при внесении изменений, скорее всего, не будет учитывать наличие подклассов.
Заключение
В заключение хотелось бы еще раз отметить, что не стоит слишком уж часто создавать подтипы. Если вы можете объявить создаваемые классы как final, то уже никто не сможет наследоваться от них и, в итоге, получить внезапный «сюрприз», при добавлении вами новых членов в суперкласс. Кроме того, каждый раз производя изменение области видимости существующих членов класса, будьте крайне осторожны и помните о потенциальной возможности наличия членов с такими же именами, как у изменяемого.
Автор: Fisfis