javajvm

Java changing variable name changes program behaviour


I found a scenario where java program behaves differently after renaming a variable. I understand this isn't actually code that anyone would use but if someone knows whats going on it would be nice to have an explanation. I tried this with java 1.6 on Eclipse Kepler.

package _test;

public class TestClass{
    public static void main(String...args){
        Object testClazz$1 = new Object (){
            public String toString() {
                return "hello";
            }
        };
        TestClass$1 test = new TestClass$1();
        System.out.println(testClazz$1.toString());
        test.doStuff();
    }
}

class TestClass$1{
    public void doStuff(){
        System.out.println("hello2");
    }
}

This outputs:

hello

Exception in thread "main" java.lang.NoSuchMethodError: _test.TestClass$1.doStuff()V at _test.TestClass.main(TestClass.java:13)

As far as I understand the compiler creates a TestClass$1.class file for the testClazz$1 object and this causes a naming collision.

But after renaming the object to testClass$1:

package _test;

public class TestClass{
    public static void main(String...args){
        Object testClass$1 = new Object (){
            public String toString() {
                return "hello";
            }
        };

        TestClass$1 test = new TestClass$1();
        System.out.println(testClass$1.toString());
        test.doStuff();
    }
}

class TestClass$1{
    public void doStuff(){
        System.out.println("hello2");
    }
}

The output is:

_test.TestClass$1@2e6e1408

hello2

Any ideas what is going on here?


Solution

  • When the classloader encounters the anonymous Object() {...} class it loads it under the name TestClass$1. This creates a conflict with class TestClass$1 {...} which was explicitly defined.

    However, class name conflicts are handled rather ungracefully. This bit of documentation tells us that

    If the class c has already been linked, then this method simply returns.

    That's what happens in your case. You only ever load one of the two TestClass$1 classes.

    The "different variable names" are not responsible for anything other than re-compiling and re-linking within your compiler. At this point, the classloader is free to pick whichever one of the two TestClass$1 likes better and use that everywhere.


    If you're using something like eclipse (like I am) then your bytecode will get cached, until a new touch operation on the source file (and updating of timestamps...). Here's what I did to reproduce (running openjdk 1.7, Eclipse Kepler under RedHat):

    Put this inside a source file TestClass.java:

    package javaclasses.classes;
    
    public class TestClass{
        public static void main(String...args){
            Object o = new Object (){
                public String toString() {
                    return "hello";
                }
            };
    
            TestClass$1 test = new TestClass$1();
            System.out.println(o.toString());
            test.doStuff();
        }
    }
    
    class TestClass$1{
        public void doStuff(){
            System.out.println("hello2");
        }
    }
    

    ctrl + F11 outputs:

    javaclasses.classes.TestClass$1@42293b53
    hello2
    

    Open this in a console and touch TestClass.java

    Go back in eclipse and ctrl + F11 now outputs:

    hello
    Exception in thread "main" java.lang.NoSuchMethodError: javaclasses.classes.TestClass$1.doStuff()V
        at javaclasses.classes.TestClass.main(TestClass.java:13)
    

    Conlusion: All that can be said definitively is that the default ClassLoader is unreliable for manually resolving classes with the same fully qualified names. Changing variable names doesn't matter, the updated timestamp on your source file does.