Wednesday, September 9, 2009

Inconstant Constants in Java

In their 2009 JavaOne presentation Return of the Puzzlers: Schlock and Awe (PDF), Joshua Bloch and Neal Gafter presented seven more puzzlers and extracted lessons learned from each of these puzzlers and their solutions. These lessons learned include old standbys such as read the documentation, ensure you are calling the correct methods, preferring composition over inheritance, and never call an overridable method from constructor. Not surprisingly, many of the lessons learned also tie into Joshua Bloch's Effective Java recommendations.

One of the interesting nuances of the Java programming language highlighted in this presentation is the concept of constant variables in Java. The presentation highlights three sections of the Java Language Specification (JLS) Third Edition (HTML/PDF): 4.12.4 ("final Variables"), 13.4.9 ("final Fields and Constants"), and 15.28 ("Constant Expression"). Section 4.12.4 ("final Variables") defines and warns about constant variables at the very end of that section (links/references to other sections of JLS omitted; emphasis included in original text):

We call a variable, of primitive type or type String, that is final and initialized with a compile-time constant expression a constant variable. Whether a variable is a constant variable or not may have implications with respect to class initialization, binary compatibility and definite assignment.


The specification and this JavaOne presentation point out that only primitives and String can be constant and that null is not a constant. They also point out problems that can occur between code binaries when constants are inlined and not all code is re-compiled together. For example, if the final keyword is added to a field and the pre-existing binaries try to set the field, an IllegalAccessError will be thrown. Going the other way, removing final behaves the same way as changing the value of a final field: the pre-existing code will not break, but it also won't realize that there is a new value.

This is demonstrated with a simple example. Suppose you have a simple class called Constants as defined below.

Constants.java - First Version

package dustin.examples.puzzlers;

/**
* The main purpose of this class is to demonstrate the problems with inlined
* constants when they are not really constant.
*/
public class Constants
{
public static final String ONE = "Uno";
public static final String TWO = "Dos";
public static final String THREE = "Tres";
public static final String FOUR = null;
public static final String FIVE = "Cinco";
}


Four of the five constants defined above have String values. However, one of them, the constant FOUR, is assigned to null.

Suppose that the Constants.java class was edited or replaced with the code below:

Constants.java - Second Version

package dustin.examples.puzzlers;

/**
* The main purpose of this class is to demonstrate the problems with inlined
* constants when they are not really constant.
*/
public class Constants
{
public static final String ONE = "Un";
public static final String TWO = "Duex";
public static final String THREE = "Trois";
public static final String FOUR = "Quatre";
public static final String FIVE = "Cinq";
}


The second version of this class maps English number names to French names for the same numbers. In this case, all five constants had Strings defined. Now suppose there was a "client" class that makes use of Constants.java with code as shown in the next listing.

NumbersTranslator.java - Client Using Constants.java

package dustin.examples.puzzlers;

import static java.lang.System.out;

public class NumbersTranslator
{
public static void main(final String[] arguments)
{
out.println("One is " + Constants.ONE);
out.println("Two is " + Constants.TWO);
out.println("Three is " + Constants.THREE);
out.println("Four is " + Constants.FOUR);
out.println("Five is " + Constants.FIVE);
}
}


If the above class, NumbersTranslator, was recompiled every time the Constants.java class it depends on was recompiled, we would not see any discrepancies. However, if we compiled NumbersTranslators against the first version (Spanish) of Constants.java and then recompiled only the second version (French) of Constants.java without recompiling NumbersTranslators, an interesting result occurs when we run the NumbersTranslators.main(). That output is shown next.



The interesting observation here is that even though the code was run with the "French version" of Constants.java, four of the five lines printed still show the Spanish versions (from first version of Constants.java file). However, the value printed for the fourth constant is the French version.

This output demonstrates two principles. First, true constant variables are inlined. This is why the Spanish versions of the constants remained for four of five lines remained despite having the French version of Constants.java on the classpath. The fact that the fourth line displayed the French version illustrates the second point: null is not a constant variable and thus is not inlined and so the actual constant on the classpath is used in that case.

The JavaOne presentation and the JLS both recommend the same two practices for dealing with this subtlety. The first recommendation, in this case from the specificaition, is: "The best way to avoid problems with 'inconstant constants' in widely-distributed code is to declare as compile time constants only values which truly are unlikely ever to change." In other words, only designate a field as static final if the field will truly never change. The specific suggests that mathematical constants fit well here. This makes sense because we don't expect these physical constants to change (or will they?). The specification goes further: "Other than for true mathematical constants, we recommend that source code make very sparing use of class variables that are declared static and final."

The second recommendation provided by both the JLS and the JavaOne presentation is to, as the JLS states, "declare a private static variable and a suitable accessor method to get its value." The specification demonstrates this with a code snippet similar to the following:


private static String ONE;
public static int getOne() { return ONE; }


Bloch and Gafter propose a similar concept. They demonstrate a general static ident method that accepts the type of the constant and returns the passed-in parameter. It would look something like this:


private static String ident(String stringConstant)
{
return stringConstant;
}

public static final String ONE = ident("uno");


The reason that both of these related approaches work is that the calling of the respective methods (getXXXX or ident) prevents inlining of the constant variables involved.

The next code listing shows the first version of Constants.java re-written to use the Bloch/Gafter approach.


package dustin.examples.puzzlers;

/**
* The main purpose of this class is to demonstrate the problems with inlined
* constants when they are not really constant.
*/
public class Constants
{
private static String ident(final String stringConstant)
{
return stringConstant;
}

public static final String ONE = ident("Uno");
public static final String TWO = ident("Dos");
public static final String THREE = ident("Tres");
public static final String FOUR = ident(null);
public static final String FIVE = ident("Cinco");
}


The output is shown next. I followed the same steps as before (building and rebuilding only the once-Spanish version of Constants.java file with the new French version), but the results are different (which is better in this case):



As the output shows, the recommended "indent" approach prevents inlining of the constant variables and allows the updated Constants.java constants to be fully reflected in the executed code.

This may not be a big deal in a development environment in which full clean and rebuild processes prevent these binary mismatches, but it has potential to lead to nasty and not-so-obvious bugs in "widely-distributed code" situations described in the specification.


Additional References

Java Constants - A nice post on this subject

Rotten Statics - Nice overview of where this bit a development team "in real life"

No comments: