A Curious Java Language Feature and How it Produced a Subtle Bug

Java’s visibility rules are tricky at times. Do you know what this will print?

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();
    }
}

It will print (highlight to see the solution):

B.x

Because:

The super type B's members hide the enclosing type C's members, 
which again hide the static import from A.

How can this lead to bugs?

The problem isn’t that the above code is tricky per se. When you write this logic, everything will work as expected. But what happens if you change things? For instance, if you mark the super type’s attribute as private:

package p;

import static p.A.x;

class A {
    static String x = "A.x";
}

class B {
    private String x = "B.x"; // Change here
}

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();
    }
}

Now, suddenly, B.x is no longer visible from within method m(), so the rules are now:

Enclosing member hides static import

And we get the result of

C.x

Of course, we can change this again to the following code:

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"; // Change here

    class D extends B {
        void m() {
            System.out.println(x);
        }
    }
}

public class X {
    public static void main(String[] args) {
        new C().new D().m();
    }
}

As we all know, 50% of variables that are no longer needed are renamed to “old”.

Now, in this final version, there’s only one possible meaning of x inside of m(), and that’s the statically imported A.x, thus the output is:

A.x

Subtleties across a larger code base

These refactorings have shown that making something less visible is dangerous because a sub type might have depended on it, but for some freak coincidence, there was another member in a less “important” scope by the same name that now jumps in and keeps you from getting a compiler error.

The same can happen if you make something that was private more visible. Suddenly, it might be in scope on all its subtypes, even if you didn’t want it to be in scope.

Likewise, with the static import, we could run into a similar issue. When your code depends on a static import, it might suddenly be hidden by some member of the same name, e.g. in a super type. The author of the change might not even notice, because they’re not looking at your subtype.

Conclusion

The conclusion is, once more, not to rely on subtyping too much. If you can make your classes final, no one will ever override them and accidentally “profit” from your newly added members.

Besides that, every time you do a visibility change of your members, be very careful about potential members that are named the same accidentally.