by Vladimir Roubtsov

When a constant isn’t really a constant

news
Mar 28, 20035 mins

Cyclic class dependencies can create unpredictable runtime behavior

March 28, 2003

Q: What are the possible negative effects of using cyclic definitions in Java

A: Normally, a Java compiler produces very “dynamic” output: you can recompile just a single class, and the rest of the application picks up the changes. This happens because the .class format uses dynamic links to reference cross-class constructs such as fields and methods, and they are resolved only at classloading time.

In this article, I describe a well-known exception to this behavior and an exception to the exception that is fairly rare but can cause very subtle and hard-to-see errors. Cyclic field definitions cause the latter exception, which leads to somewhat nondeterministic execution in Java.

Inlined static final constants

Consider the following two definitions:

public class Main implements InterfaceA
{
    public static void main (String [] args)
    {
        System.out.println (A);
    } 
} // End of class
public interface InterfaceA
{
    public static final int A = 1;
} // End of interface

According to the Java Language Specification, any static final field initialized with an expression that can be evaluated at compile time must be compiled to byte code that “inlines” the field value. That is, no dynamic link will be present inside class Main telling it to obtain the value for A from InterfaceA at runtime. Instead, the literal 1 will compile into Main.main() directly. You can confirm this by examining the javap dump for the method:

Method void main(java.lang.String[])
   0 getstatic #23 
   3 iconst_1
   4 invokevirtual #29 
   7 return

The iconst_1 instruction above pushes integer value 1 to the JVM operand stack before invoking System.out.println() on it. The value is embedded into byte code, and no link to Interface.A remains. If you recompile InterfaceA.java so that field A is now, say, 2, but do not recompile Main.java, Main.main()‘s output stays the same.

The above is well known to any experienced Java programmer. This slightly odd Java feature calls for extra attention when using any kind of Java make tool that can incrementally recompile Java sources based on file modification timestamps only (see Note 1). On the other hand, this feature is sometimes handy for implementing a poor man’s version of conditional compilation in Java.

It looks like a constant but it isn’t…

Note that for constant inlining to happen, several conditions must take place simultaneously. For example, it won’t occur if the field initializer expression can be evaluated only at runtime:

public interface InterfaceA
{
    public static final int A = new java.util.Random ().nextInt ();
} // End of interface

With this change in InterfaceA, the byte code for Main.main() becomes:

Method void main(java.lang.String[])
   0 getstatic #27 
   3 getstatic #31 
   6 invokevirtual #37 
   9 return

Observe the now dynamic reference to the field A value in the byte code.

The above change in Interface.A was quite conspicuous. However, imagine instead that the application uses the following three interfaces:

public interface InterfaceA
{
    public static final int A = 2 * InterfaceB.B;
} // End of interface
public interface InterfaceB
{
    public static final int B = InterfaceC.C + 1;
} // End of interface
public interface InterfaceC extends InterfaceA
{
    public static final int C = A + 1;
} // End of interface

Try this version of main():

public class Main implements InterfaceA, InterfaceB, InterfaceC
{
    public static void main (String [] args)
    {
        System.out.println (A + B + C);
    } 
} // End of class

It prints 7. Ignore this value for now and change main() in a seemingly innocuous way:

public class Main implements InterfaceA, InterfaceB, InterfaceC
{
    public static void main (String [] args)
    {
        System.out.println (C + B + A); // The sum is still the same, right?
    } 
} // End of class

The result is now 6. Reordering summation terms seems to have changed the result. Did you expect that? Let’s see why this happened.

Although all initializer expressions for fields A, B, and C look like they could be evaluated at compile time, they can’t because of the cycle in their definitions. A depends on B; B depends on C; and, C depends on A.

As a result, the compiler will not do any inlining and instead generates static initializer code (see Note 2) that sets all three fields at classloading/initialization time. An important side effect is that every expression containing any of these fields now depends on the order of classloading by the application. To work out the first main() result, I note that InterfaceA is the first interface to load (the summation operands are evaluated left to right), but InterfaceC is the first interface to finish initialization (because of the dependency direction). Field InterfaceC.C depends on InterfaceA.A and is set while InterfaceA‘s initialization is still pending (hence, field A is still at the default zero value). When all is said and done, C ends up as 1, B as 2, and finally A as 4, which adds up to 7. I leave working out the last main() version as an exercise to readers. (Hint: All three values will end up differing.)

Perhaps my example looks contrived. However, imagine the same three interfaces defined in distinct packages scattered throughout a large code base: noticing the cyclic definition will not be quite so easy then. Each individual definition looks like it could be inlined, and a casual glance will not detect any issues. However, such code causes very obscure problems later on: although some expressions’ values will be reproducible between runs of the same version of your application, changing working code can cause a different classloading order and an unpredictably new execution path somewhere. Alternatively, unpredictable changes in concurrent thread scheduling can cause different classloading orders. Unfortunately, most compilers do not consider such code erroneous or at least worthy of a warning to the programmer.

Vladimir Roubtsov has programmed in a variety of languages for more than 13 years, including Java since 1995. Currently, he develops enterprise software as a senior engineer for Trilogy in Austin, Texas.