Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

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

DZone's Guide to

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

Sometimes seemingly innocuous features can produce bugs, however subtle. In this post, we look at how relying on subtyping too much can result in a bug.

· Java Zone
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

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:

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 accidentally named the same.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
java ,bug ,subtyping ,refactoring ,tutorial

Published at DZone with permission of Lukas Eder, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}