Saturday, February 12, 2011

Java static initialization - part 1

Details of java static initialization (i.e. initialization of classes and interfaces) are defined in Java Language Specification (JLS) - §12.4. This post goes through the specification and provides additional comments and code examples. Please note that any quotations are taken from JLS if not said otherwise.

12.4 Initialization of Classes and Interfaces

Initialization of a class consists of executing its static initializers and the initializers for static fields (class variables) declared in the class. Initialization of an interface consists of executing the initializers for fields (constants) declared there.
Detail specification of static initializer is available in JLS - 8.7 Static Initializers and specification of initializer for static fields is in JLS - 8.3.2 Initialization of Fields. This little class should be a sufficient remainder for above:
public class A {
    static {
        System.out.println("This is a static initializer");
    }
    static public String staticField = "This is initializer for static field";
}
Interface does not have static initializers and its fields are always static, public and final.
Before a class is initialized, its superclass must be initialized, but interfaces implemented by the class are not initialized. Similarly, the superinterfaces of an interface are not initialized before the interface is initialized.
It means the following program:
interface I{
    String X = TestClassHierarchy.out("I ");
}
 
class Super implements I{
    static String x = TestClassHierarchy.out("Super ");
}
 
class Sub extends Super{
    static String x = TestClassHierarchy.out("Sub ");
}
 
public class TestClassHierarchy {
    static public void main(String[]arg){
        new Sub();
    }
    static String out(String text){ System.out.print(text); return text; }
}

prints:

Super Sub

There is another example program regarding this section later in the JLS too.

12.4.1 When Initialization Occurs

Initialization of a class consists of executing its static initializers and the initializers for static fields declared in the class. Initialization of an interface consists of executing the initializers for fields declared in the interface.

Before a class is initialized, its direct superclass must be initialized, but interfaces implemented by the class need not be initialized. Similarly, the superinterfaces of an interface need not be initialized before the interface is initialized.

This section just repeats first two paragraphs without an apparent reason...
A class or interface type T will be initialized immediately before the first occurrence of any one of the following:
This is the interesting part. JLS is quite often benevolent to give enough freedom to JVM implementators for various optimizations. But it is strict in this case. Initialization is done on these events below. It can't be done sooner - or later for that matter. (Note: This is why Initialization On Demand Holder idiom works.)
  • T is a class and an instance of T is created.
T instance can be created using:
  1. a new keyword - new T()
  2. reflection - e.g. T.class.newInstance()
  3. deserialization
  4. cloning
Obviously only 1) - 3) are relevant here and can cause a static initialization.
  • T is a class and a static method declared by T is invoked.
So this program:
class A {
    static {
        System.out.print("class A initialized, ");
    }
 
    static public void staticMethod(){
        System.out.print("static method called, ");
    }
}

public class TestStaticMethodInit {
    public static void main(String[] args) {
        A.staticMethod();
    }
}

prints:

class A initialized, static method called,
  • A static field declared by T is assigned.
And this program:
class A {
    static { System.out.print("class A initialized, "); }
    static public int X;
}

public class TestStaticFieldAssigned {
    public static void main(String[] args) {
        A.X = 3;
    }
}

prints:

class A initialized,
  • A static field declared by T is used and the field is not a constant variable (§4.12.4).

A constant variable exception makes this item a bit more complicated. Section 4.12.4 says that the "constant variable" is "a variable, of primitive type or type String, that is final and initialized with a compile-time constant expression (§15.28)". The exact definition of compile-time constant expression is in §15.28 but in short - a compile-time constant expression, as name suggests, is an expression which can be evaluated by compiler. It can be composed of primitive and string literals, operators and other compile-time constant expressions but usually it is just simple literal. Some of the §15.28's more complicated constant expression examples are (short)(1*2*3*4*5*6) and "The integer " + Long.MAX_VALUE + " is mighty big.".

Note: Be aware that despite their constant-like look and feel the Boolean.TRUE/Boolean.FALSE are not constant expressions.

Example time. This program:

class A {
    static { System.out.print("class A initialized, "); }
    static public final String X = "string";
}

public class TestStaticFieldUsed {
    public static void main(String[] args) {
        System.out.println(A.X);
    }
}

prints only:

string

But if you change X initialization to new String("string") or "string".intern() then program prints:

class A initialized, string

because these are not constant expressions.

Please note the "string" and "string".intern() will initialize X to the identical object though.

  • T is a top-level class, and an assert statement (§14.10) lexically nested within T is executed.
If it is not clear what scenario is described here then the §14.10 gives a clue (highlight added):

Note that an assertion that is enclosed by a top-level interface does not cause initialization.

Usually, the top level class enclosing an assertion will already be initialized. However, if the assertion is located within a static nested class, it may be that the initialization has not taken place.

So this program (with assertions enabled):
public class TestInitializationAssert {
    public static void main(String[] args) {
        new A.AInner();
    }
}
----------------
public class A{
    static {
        System.out.print("init A, ");
    }
    static public class AInner{
        static {
            assert Boolean.TRUE;
            System.out.print("init AInner, ");
        }
    }
}

should print:

init AInner, init A,

But it actually does not. It prints only:

init AInner,

The reason why it does not work as expected is Bug ID: 6394986 JLS3 14.10 An assert statement must cause the enclosing top level class... The problem exists on both Sun and IBM JDK's.

Invocation of certain reflective methods in class Class and in package java.lang.reflect also causes class or interface initialization.
The above list of static initialization triggers works in the case of the reflection too. The point is it does not matter how exactly these triggers are activated but when it happens static initializations should happen.
A class or interface will not be initialized under any other circumstance.
As mentioned above JLS is strict in this case.
The intent here is that a class or interface type has a set of initializers that put it in a consistent state, and that this state is the first state that is observed by other classes. The static initializers and class variable initializers are executed in textual order, and may not refer to class variables declared in the class whose declarations appear textually after the use, even though these class variables are in scope (§8.3.2.3). This restriction is designed to detect, at compile time, most circular or otherwise malformed initializations.
This section introduces the crucial rule for static initialization of the class/interface - execution happens in textual order. It means this program:
public class TestStaticInitializtionOrdering {
    static { out("initialized first, ");}

    public static void main(String[] args) {
        out("class is initialized");
    }

    static public String X = out("initialized second, ");
 
    private static String out(String text) {
        System.out.print(text);
        return text;
    }
 
    static { out("initialized third, ");}
}

prints:

initialized first, initialized second, initialized third, class is initialized

Above section also mentions §8.3.2.3. Other sections of §8.3.2 - Initialization of Fields discuss static initialization too but §8.3.2.3 is the most relevant of them in this context and it is quoted fully below:

8.3.2.3 Restrictions on the use of Fields during Initialization

The declaration of a member needs to appear textually 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.
  • The usage is via a simple name.
  • C is the innermost class or interface enclosing the usage.
A compile-time error occurs if any of the four requirements above are not met. This means that a compile-time error results from the test program:
class Test {
    int i = j; // compile-time error: incorrect forward reference
    int j = 1;
}
whereas the following example compiles without error:

class Test {
    Test() { k = 2; }
    int j = 1;
    int i = j;
    int k;
}
even though the constructor (§8.8) for Test refers to the field k that is declared three lines later. These restrictions are designed to catch, at compile time, circular or otherwise malformed initializations. Thus, both:
class Z {
    static int i = j + 2; 
    static int j = 4;
}
and:
class Z {
    static { i = j + 2; }
    static int i, j;
    static { j = 4; }
}
result in compile-time errors. Accesses by methods are not checked in this way, so:
class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}
produces the output: 0 because the variable initializer for i uses the class method peek to access the value of the variable j before j has been initialized by its variable initializer, at which point it still has its default value (§4.12.5).

A more elaborate example is:

class UseBeforeDeclaration {
    static {
        x = 100; // ok - assignment
        int y = x + 1; // error - read before declaration
        int v = x = 3; // ok - x at left hand side of assignment
        int z = UseBeforeDeclaration.x * 2; 
        // ok - not accessed via simple name
        Object o = new Object(){ 
            void foo(){x++;} // ok - occurs in a different class
            {x++;} // ok - occurs in a different class
        };
    }
    {
        j = 200; // ok - assignment
        j = j + 1; // error - right hand side reads before declaration
        int k = j = j + 1; 
        int n = j = 300; // ok - j at left hand side of assignment
        int h = j++; // error - read before declaration
        int l = this.j * 3; // ok - not accessed via simple name
        Object o = new Object(){ 
            void foo(){j++;} // ok - occurs in a different class
            { j = j + 1;} // ok - occurs in a different class
        };
    }
    int w = x = 3; // ok - x at left hand side of assignment
    int p = x; // ok - instance initializers may access static fields
    static int u = (new Object(){int bar(){return x;}}).bar();
    // ok - occurs in a different class
    static int x;
    int m = j = 4; // ok - j at left hand side of assignment
    int o = (new Object(){int bar(){return j;}}).bar(); 
    // ok - occurs in a different class
    int j;
}
The examples at the end of the quote is descriptive and very helpful in understanding of the next section.
As shown in an example in §8.3.2.3, the fact that initialization code is unrestricted allows examples to be constructed where the value of a class variable can be observed when it still has its initial default value, before its initializing expression is evaluated, but such examples are rare in practice. (Such examples can be also constructed for instance variable initialization; see the example at the end of §12.5). The full power of the language is available in these initializers; programmers must exercise some care. This power places an extra burden on code generators, but this burden would arise in any case because the language is concurrent (§12.4.3).

Some of the static initialization scenarios are really tricky so the safest thing to do is to stick to the proper action ordering - declare, initialize, use. Otherwise one might be really surprised what is going on especially if non-standard static initialization order is combined with other Java features.

This program is inspired by one of the Java puzzlers:

public class TestStaticInitOrderWithAutoboxing {

    static{System.out.println(getYesOrNo() ? "yes" : "no");}
 
    static final public Boolean YES_OR_NO = Boolean.TRUE;
 
    static boolean getYesOrNo(){ return YES_OR_NO;};
 
    public static void main(String[] args) {}
}

The program above does print neither yes nor no but it throws NullPointerException instead. The class name gives a subtle hint - it is caused by adding autoboxing to the mix. The reason is getYesOrNo() method called in line 3 is supposed to return a value of YES_OR_NO which has not been initialized yet. Because YES_OR_NO is of Boolean class type so it has null value when uninitialized. As getYesOrNo() method returns boolean primitive, it tries to unbox null which cause the exception. The fix is straightforward - move line 5 ahead of line 3 so the "declare, initialize, use" rule is satisfied.

As a little exercise: what will program do when YES_OR_NO's type is changed from Boolean to boolean?

The section 12.4.1 ends with 3 commented examples.

Before a class is initialized, its superclasses are initialized, if they have not previously been initialized.

Thus, the test program:

class Super {
    static { System.out.print("Super "); }
}
class One {
    static { System.out.print("One "); }
}
class Two extends Super {
    static { System.out.print("Two "); }
}
class Test {
    public static void main(String[] args) {
        One o = null;
        Two t = new Two();
        System.out.println((Object)o == (Object)t);
    }
}

prints:

Super Two false

The class One is never initialized, because it not used actively and therefore is never linked to. The class Two is initialized only after its superclass Super has been initialized.

A reference to a class field causes initialization of only the class or interface that actually declares it, even though it might be referred to through the name of a subclass, a subinterface, or a class that implements an interface.

The test program:

class Super { static int taxi = 1729; }
class Sub extends Super {
    static { System.out.print("Sub "); }
}
class Test {
    public static void main(String[] args) {
        System.out.println(Sub.taxi);
    }
}

prints only:

1729
because the class Sub is never initialized; the reference to Sub.taxi is a reference to a field actually declared in class Super and does not trigger initialization of the class Sub.

Initialization of an interface does not, of itself, cause initialization of any of its superinterfaces.

Thus, the test program:

interface I {
    int i = 1, ii = Test.out("ii", 2);
}
interface J extends I {
    int j = Test.out("j", 3), jj = Test.out("jj", 4);
}
interface K extends J {
    int k = Test.out("k", 5);
}
class Test {
    public static void main(String[] args) {
        System.out.println(J.i);
        System.out.println(K.j);
    }
    static int out(String s, int i) {
        System.out.println(s + "=" + i);
        return i;
    }
}

produces the output:

1
j=3
jj=4
3

The reference to J.i is to a field that is a compile-time constant; therefore, it does not cause I to be initialized. The reference to K.j is a reference to a field actually declared in interface J that is not a compile-time constant; this causes initialization of the fields of interface J, but not those of its superinterface I, nor those of interface K. Despite the fact that the name K is used to refer to field j of interface J, interface K is not initialized.

To sum the post:

  • Static fields and static blocks of a class are initialized in the textual order
  • Interface fields are initialized in the textual order
  • Static initialization is triggered solely by these events - instance creation, static method call, assignment to static field, usage of static non-constant variable field and execution of an nested assert statement
  • Before static initialization of the class starts, a static initialization of the super class is done in case it has not been initialized yet
  • Usage of static field before initialization should be avoided

Part 2 is available here.

3 comments:

  1. Hi

    The below code creates two instances of Logger, couldnt figure out why.
    -------------------------
    public class Test{

    static protected Logger _logger = LogManager.getLogger(LogConst.compTraceNames[LogConst.COMP]);
    static protected Logger log = org.apache.log4j.Logger.getLogger("com.aaa.TelnetMgr");
    public static void main(String[] args) {

    for( Enumeration em=LogManager.getCurrentLoggers(); em.hasMoreElements(); ) {
    Logger logger = (Logger)em.nextElement();
    String lName = logger.getName();
    System.out.println(lName + " - " + logger);
    }

    }
    }

    ----------
    public interface LogConst {
    public int COMP = 1;
    public String [] compTraceNames = { "com.aaa.General ", "com.aaa.TelnetMgr ",};
    }


    Logger with static modifier, should get initialized once when class is loaded, but it gets two instance instead of one. Removing the static modifier solves my problem.What is the relevance that it creates two with a static modifier.

    ReplyDelete
    Replies
    1. Hi Sidharth,

      the creation of 2 loggers has nothing to do with static modifier. Removing of static modifier just cause that the getLogger method is not called at all. If you remove both static modifiers, then no logger is created.

      The problem is typo in compTraceNames[1] - "com.aaa.TelnetMgr ". If you remove the space at the end, you'll get single Logger as expected.

      Delete
    2. Jaroslav,

      I'would say your eyes are very sharp indeed. :-) .Thanks a lot.

      Delete