views:

211

answers:

6

I have this code

private static Set<String> myField;

static {
    myField = new HashSet<String>();
    myField.add("test");
}

and it works. But when I flip the order, I get an illegal forward reference error.

static {
    myField = new HashSet<String>();
    myField.add("test"); // illegal forward reference
}

private static Set<String> myField;

I'm a little bit shocked, I didn't expect something like this from Java. :)

What happens here? Why is the order of declarations important? Why does the assignment work but not the method call?

+1  A: 

In Java, all initializers, static or otherwise, are evaluated in the order in which they appear in the class definition.

skaffman
But I think the question is *why*, isn't it ?
Brian Agnew
Why does that prohibit calling the add method but not the assignment?
DR
+9  A: 

First of all, let's discuss what a "forward reference" is and why it is bad. A forward reference is a reference to a variable that has not yet been initialized, and it is not confined only to static initalizers. These are bad simply because, if allowed, they'd give us unexpected results. Take a look at this bit of code:

public class ForwardRef {
    int a = b; // <--- Illegal forward reference
    int b = 10;
}

What should j be when this class is initialized? When a class is initialized, initializations are executed in order the first to the last encountered. Therefore, you'd expect the line

a = b;

to execute prior to:

b = 10;

In order to avoid this kind of problems, Java designers completely disallowed such uses of forward references.

EDIT

this behaviour is specified by section 8.3.2.3 of Java Language Specifications:

The declaration of a member needs to appear before it is used only if the member is an instance (respectively static) field of a class or interface C and all of the following conditions hold:

  • The usage occurs in an instance (respectively static) variable initializer of C or in an instance (respectively static) initializer of C.

  • The usage is not on the left hand side of an assignment.

  • C is the innermost class or interface enclosing the usage.

A compile-time error occurs if any of the three requirements above are not met.

dfa
OK, I understand. But after the initial assignment myField *is* initialized. Why can't I still not call the add method?
DR
If those three requirements were not there, I could create an implicit forward reference using local variables in the initializer, right? Is that the reason for these restrictions?
DR
ths JLS says: "...These restrictions are designed to catch, at compile time, circular or otherwise malformed initializations...."
Carlos Heuberger
@dfa: You are quoting JLS 2.0. In JLS 3.0, this section is expressed much more clearly: http://java.sun.com/docs/books/jls/third_edition/html/j3TOC.html
Stephen C
A: 

I think the method call is problematic because the compiler cannot determine which add() method to use without a reference type for myField.

At runtime, the method used will be determined by the object type, but the compiler only knows about the reference type.

FarmBoy
I don't think that's the problem, because with virtual methods the compiler almost never can bind the method at compile time. That's where the VMT is used: http://en.wikipedia.org/wiki/Virtual_method_table
DR
+1  A: 

See the rules for forward references in the JLS. You cannot use forward references if:

  • The usage occurs in an instance (respectively static) variable initializer of C or in an instance (respectively static) initializer of C.
  • The usage is not on the left hand side of an assignment.
  • The usage is via a simple name.
  • C is the innermost class or interface enclosing the usage.

Since all of these hold for your example, the forward reference is illegal.

Andrzej Doyle
+2  A: 

try this:

class YourClass {
    static {
        myField = new HashSet<String>();
        YourClass.myField.add("test");
    }

    private static Set<String> myField;
}

it should compile without errors according the JLS...
(don't really help, or?)

Carlos Heuberger
I haven't tested this, but this still violates the "left-hand rule" in the JLS. See my answer below for why.
rtperson
I've tested it and it works. Now I don't understand anything anymore...
DR
@DR: this is a compiler feature. Reflection can be used to circumvent many compiler checks, e.g. calling a private method from outside the class.
Robert Munteanu
Wow. +1, and a tip of the hat, to Carlos for getting this one right.
rtperson
@rtperson: it is also a *violation* of the 3rd rule: "usage is via a simple name". IMO the JLS is *difficult* to read: "The declaration of a member needs to appear textually before it is used only if ... and all of the following conditions hold...", but the JLS also gives that example "int z = UseBeforeDeclaration.x * 2; // ok - not accessed via simple name"
Carlos Heuberger
@Carlos: The JLS is difficult, but I get it now. The intro to Chapter 6 distinguishes between simple names (i.e., just the name of the variable and the method -- myField.add()) and the qualified name (i.e., YourClass.myField.add()). The forward-reference error is thrown only if *all* the rules in 8.2.3.2 are broken, so changing the name from simple to qualified allows it to work. They added the simple name rule in the third edition of the JLS, so it makes sense that the mechanism here is via reflection. Very interesting.
rtperson
+1  A: 

To elaborate on DFA's answer:

I think what's tripping you up is the "left hand side" rule in the second bullet point in JLS 8.2.3.2. In your initialization, myField is on the left-hand side. In your call to add, it's on the right-hand side. The code here is implicitly:

boolean result = myField.add('test')

You're not evaluating the result, but the compiler still acts as if it's there. That's why your initialization passes while your call to add() fails.

As for why this is so, I have no idea. It may well be for the convenience of the JVM developers, for all I know.

rtperson
If that's the case, shouldn't a void method work?
DR
Well, no, for exactly the same reason. A call that returns void is still on the right-hand side, since you're not assigning into it. You've merely told the compiler that the function doesn't have any output, but that doesn't change that it is a function.
rtperson