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:
Post a Comment