Привет, Хабр! Представляю вашему вниманию перевод статьи 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, то уже никто не сможет наследоваться от них и, в итоге, получить внезапный «сюрприз», при добавлении вами новых членов в суперкласс. Кроме того, каждый раз производя изменение области видимости существующих членов класса, будьте крайне осторожны и помните о потенциальной возможности наличия членов с такими же именами, как у изменяемого.

Комментарии (7)


  1. AndreyRubankov
    11.09.2017 14:15
    +4

    Есть отлично правило хорошего кода: «Используйте только статические внутренние классы».
    И есть еще лучше: «Старайтесь не использовать внутренние классы вообще».

    Ситуации, когда действительно нужен не статический внутренний класс, крайне редкие. Если вам встретился такой, то скорее всего вы наткнулись на ошибку или на плохой код, который нужно переписать.


    1. Mountaineer
      12.09.2017 09:22

      Здрасти. Анонимный класс — пример использования не статического внуттренего класса, который был очень оппулярен до появления лямбд. Например всякие хендлеры для GUI, тот же new Thread(new Runnable() {...} ); и т.д.


      1. AndreyRubankov
        12.09.2017 09:44

        Доброго дня. Анонимные и Внутренние классы – это разные вещи.


        1. AndreyRubankov
          12.09.2017 09:56
          +3

          Для анонимных классов есть правило: «У анонимного класса не должно быть состояния».

          Если нужно состояние – сделайте это обычным классом; а если при этом нужно связать с текущим – передайте ссылку на текущий, методы для взаимодействия сделайте package-private.

          пояснение: Если у класса есть состояние, значит у него довольно сложная логика и этот класс должен быть протестирован через те же unit-тесты. Его поддержка внутри другого класса будет не тривиальной, потому следует разделить эти 2 сущности.


  1. izzholtik
    11.09.2017 14:31
    +2

    Мне кажется, всё логично. Текущий контекст перекрывает более общий.
    Тем более, что даже ванильный компилятор будет на такой код предупреждение выбрасывать.


  1. CyberSoft
    11.09.2017 22:33

    Много всякого java-кода видел, а такого ещё нет. Это нужно специально наступить на такие грабли…


  1. webkumo
    12.09.2017 03:48
    +3

    Как все мы знаем 50% переменных, которые более не будут использоваться переименовываются и получают приставку “old”.

    А я, дурак, думал, что их принято аннотировать @deprecated