views:

133

answers:

3

When passing a final object (A String in the code below) it shows up as null when printed from the anonymous inner class. However, when a final value type or straight final String is passed in, its value is correctly displayed. What does final really mean in context of the anonymous inner class and why is the object passed null?

public class WeirdInners
{
    public class InnerThing
    {
        public InnerThing()
        {
            print();
        }

        public void print(){

        }
    }

    public WeirdInners()
    {
        final String aString = "argh!".toString();
        final String bString = "argh!";
        System.out.println(aString);
        System.out.println(bString);


        InnerThing inner =new InnerThing(){
            public void print()
            {
                System.out.println("inner"+aString); // When called from constructor, this value is null.
                System.out.println("inner"+bString); // This value is correctly printed.
            }
        };

        inner.print();
    }


    public static void main(String[] args)
    {
        WeirdInners test1 = new WeirdInners();
    }

}

This is very strange behavior to me because the expectation is that the String is an object, why does calling toString() change things?

Other info: this behavior is only observed using Java 1.4, not in Java 5. Any suggestions on a workaround? Not calling toString() on an existing String is fair enough, but since this is only an example, it has real world implications if I perform it on a non-String object.

+5  A: 

If you check the section on compile-time constants in the JLS, you'll see that calling .toString() does make a difference. As does nonsense like prefixing with false?null+"":.

What is important here is the relative ordering of setting the fields that are closed over and the constructor. If you use -target 1.4 or later (which is not the default in 1.4!) then the fields will be copied before calling the super. With the spec before 1.3 this was illegal bytecode.

As is often the case in these cases, javap -c is useful to see what the javac compiler is doing. The spec is useful for understanding why (should you have sufficient patience).

Tom Hawtin - tackline
+1. You must have JLS at your bedside table :-)
ChssPly76
A: 

My guess would be that you trigger undefined behaviour when the constructor of InnerThing() passes (implicitly) its this to the print method of an anonymous subclass of InnerThing while the object is not fully constructed. This this in turn relies on an implicit reference to the this of WierdInners.

Calling .toString() moves initialisation of aString from compile time to runtime. Why the undefined behaviour differs between Java 1.4 and 1.5 is a JVM implementation detail probably.

rsp
A: 

It is dangerous to invoke overridden methods from a superclass constructor, as they will be called before the subclass-part of the object is initialized.

Moreover, an inner class accessing final variables of an enclosing scope will actually access copies of those variables (that's why they need to be final), and these copied fields reside in the anonymous subclass.

I suspect the reason bString is treated differently is that its value is known at compile time, which allows the compiler to inline the field access in the subclass, making the time that field is initialized irrelevant.

meriton