Substitution Constants

Substitution constants are the simplest type of constants. Basically, they are substituted for something else in code. See Example 7-1.

Example 7-1. Substitution constants
package oracle.hcj.constants;
public class SubstitutionConstants {
 /** Holds the logging instance. */
 private static final Logger LOGGER = Logger.getLogger(SubstitutionConstants.class);
 /** A value for PI. */
 public static final double PI = 3.141;
 public double getCircleArea(final double radius) {
 double area = (Math.pow(radius, 2) * PI);
 LOGGER.debug("The calculated area is " + area);
 return area;
 }
 public float calculateInterest(final float rate, final float principal) {
 final String LOG_MSG1 = 
 "Error: The interest rate cannot be less than ";
 final String LOG_MSG2 = ". The rate input = ";
 final double MIN_INTEREST = 0.0;
 // -- if (rate < MIN_INTEREST) {
 LOGGER.error(LOG_MSG1 + MIN_INTEREST + LOG_MSG2 + rate);
 throw new IllegalArgumentException("rate");
 }
 return principal * rate;
 }
}


This code declares several substitution constants. The constant PI has class scope while the constants LOG_MSG1, LOG_MSG2, and MIN_INTEREST have method scope. All of these constants are declared and then later substituted in various parts of the app. In the case of the public final static primitives and String objects, the substitution occurs at compile time, according to the rules discussed in . In all other cases, it occurs at runtime. The use of substitution constants is standard in most apps. However, there are a few things to note, even in this mundane bit of code. For instance, the only constant declared in the class scope is a public constant. If you remember the discussion of the final keyword, you will understand why. Since method-scoped constants are not accessible outside the method, it would be impossible to declare a public method-scoped constant. All of the other constants are used within only one method, so it would make no sense to declare them in class scope. Also note that a 2 was placed in the line in which the area of the circle was calculated. Although a constant could be used for this number, it would be wasteful for two reasons. First, the number 2 is part of the formula for the area of a circle. Second, it is used only at one particular point in the method and has no effect on any other part of the method. You should avoid creating constants if they fall under such conditions. However, MIN_INTEREST is a constant in the calculateInterest( ) method. This is because the minimum interest rate may change, and this interest rate is used twice in the code. If it ever does have to be changed, it's better to do so in one place instead of several. While these rules are applicable to numerical values, String values are a little different. Whether to convert a String literal that is used only once to a constant generally depends on how the String is used. To illustrate, let's look at the following code fragment:

package oracle.hcj.constants;
public class SubstitutionConstants {
 public float calculateInterest(final float rate, final float principal) {
 final String LOG_MSG1 = 
 "Error: The Interest rate cannot be less than ";
 final String LOG_MSG2 = ". The rate input = ";
 final double MIN_INTEREST = 0.0;
 // -- if (rate < MIN_INTEREST) {
 LOGGER.error(LOG_MSG1 + MIN_INTEREST + LOG_MSG2 + rate);
 throw new IllegalArgumentException("rate");
 }
 return principal * rate;
 }
 protected String buildLogMessage(final Exception ex) {
 StringBuffer buf = new StringBuffer(200);
 StackTraceElement[] elements = ex.getStackTrace( );
 buf.append("The exception ");
 buf.append(ex.getClass( ).getName( ));
 buf.append(" occurred at ");
 buf.append(Calendar.getInstance( ).getTime( ).toString( ));
 buf.append(" on line ");
 buf.append(elements[0].getLineNumber( ));
 return buf.toString( );
 }
}


In this code, constants are used in the first method but not in the second. Since the strings in question are used only once in both methods, the question of whether to use constants may seem arbitrary. However, there is a good reason why they are used in the first method: they improve readability. In the first method, the String objects used in the log message are long. If you tried to put them together within the structure of the log message, the result would be a long, multiline concatenation of strings. This would clearly be less readable. However, in the second method, the String objects are short, so it wouldn't make much sense to turn them into constants. On the other hand, if the String object is being used more than once, it should be a constant. Although they appear to be trivial, you need to consider substitution constants when designing your code. They can turn messy, unreadable code into code that is easy to understand.

Internationalizing with Substitution Constants

Internationalizing an app's strings gives you the perfect opportunity to show off the power of substitution constants. Although most programs are internationalized in other ways, using substitution constants can eliminate a great deal of overhead from the process. To understand how this is done, you must first learn how to internationalize strings in an app. You can then use this technique to improve the performance and maintainability of your apps.

Common internationalization techniques

To internationalize a program, a class called ResourceBundle is used. A resource bundle allows you to access strings within a properties file. To use this bundle, build a small class called I18N to load the bundle and access it. Consequently, you will not have to initialize the bundle for every class that wants to use the strings. Example 7-2 shows the code for I18N.

Example 7-2. A resource bundle accessor
package oracle.hcj.constants;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
public final class I18N {
 private final static String BUNDLE_NAME = "oracle/hcj/constants/I18N";
 private final static ResourceBundle BUNDLE=
 ResourceBundle.getBundle(BUNDLE_NAME);
 private I18N( ) {
 }
 public static String getString(final String key) {
 try {
 return BUNDLE.getString(key);
 } catch (final MissingResourceException ex) {
 return '!' + key + '!';
 }
 }
}


When the class is statically initialized, the resource bundle is initialized. To initialize the bundle, the ResourceBundle class looks for a file with the same name as BUNDLE_NAME in the classpath. In this case, it finds the I18N.properties file, which stores a list of key/value pairs that looks like the following:

# I18N.properties OK=OK CANCEL=Cancel REFRESH=Refresh


Once the resource bundle is initialized, you can use getString( ) to fetch a string value for any key. If the value isn't found, simply return the key enclosed in exclamation points. Using the resource bundle allows you to store the actual value of your strings in a file that doesn't need to be recompiled if you want to change the strings. This is beneficial to the program's translators, who are often not programmers themselves. They simply need to find all of the I18N.properties files in a project and change them with a text editor. To use the new I18N class, call getString( ):

package oracle.hcj.constants;
public class SomeApp {
 public final static void main(final String[] args) {
 System.out.println(I18N.getString("OK"));
 System.out.println(I18N.getString("CANCEL"));
 System.out.println(I18N.getString("REFRESH"));
 System.out.println(I18N.getString("SOME_STRING"));
 }
}


If you run this class, it will produce the following output:

C:\...\hcj>java -cp build/ oracle.hcj.constants.SomeApp OK Cancel Refresh
!SOME_STRING!


Screenshot

This class is run differently than the other examples because the locale will be altered in a later run.


The class found all of the strings except SOME_STRING. Although this code is useful, what makes resource bundles magical is that they automatically find the resources for the locale that is set in the virtual machine. You can write another file for German versions of your strings and it will select the correct file at runtime. The German version looks like the following:

# I18N_de.properties OK=OK CANCEL=Abbrechen REFRESH=Aktualisieren


You now have one file named I18N.properties and one named I18N_de.properties. The first file contains the English strings; the second contains the German strings. Now the ResourceBundle class will find the correct file based on the locale of the virtual machine. To change the locale, pass the user.language property to the virtual machine. Rerun the example using the German locale:

C:\...\hcj>java -Duser.language=de -cp build/ oracle.hcj.constants.SomeApp OK Abbrechen Aktualisieren
!SOME_STRING!


The ResourceBundle class magically found the other strings and printed out the German versions to the console. Since SOME_STRING also isn't defined in the German file, it was returned in the same way as in the previous run. If the ResourceBundle class can't find a version of the bundle for your particular locale, it will default to the version with no extension:

C:\dev\hcj>java -Duser.language=fr -cp build/ oracle.hcj.constants.SomeApp OK Cancel Refresh
!SOME_STRING!


Since you don't have an I18N_fr.properties file, the ResourceBundle class used the I18N.properties file instead.

Improving internationalization performance

Although the internationalization technique shown in the last section is useful, it has a few problems. When the resource bundle is initialized, it is loaded with the key/value pairs. Whenever you access it using getString( ), the bundle looks up the key in the hash map. While the program is executing, these keys don't change at all. Therefore, it would be wasteful to use a large data structure that takes up memory to accommodate them. The second problem with the previous internationalization technique is that the keys are given to the resource bundle as quoted strings. This is annoying for two reasons. First, it introduces the possibility of a typo in a key. In your app, finding a typo would be easy. In an app with hundreds of classes accessing these strings, it wouldn't be so much fun; you would have to search for every class that used the string and fix it. The second annoyance is that whenever you look up a resource, you have to type several lines of code, which increases the size of your app and makes it look ugly. You can make internationalization fast, clean, and neat by using substitution constants. All you have to do is rewrite the I18N class:

package oracle.hcj.constants;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
public final class I18N2 {
 public static final String OK;
 public static final String CANCEL;
 public static final String REFRESH;
 static {
 final String BUNDLE_NAME = "oracle/hcj/constants/I18N";
 final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME);
 
 // --
 OK = getString(BUNDLE, "OK");
 CANCEL = getString(BUNDLE, "CANCEL");
 REFRESH = getString(BUNDLE, "REFRESH");
 }
 private I18N2( ) {
 }
 private static String getString(final ResourceBundle bundle, final String key) {
 try {
 return bundle.getString(key);
 } catch (final MissingResourceException ex) {
 assert (false) : ex.getMessage( );
 return '!' + key + '!';
 }
 }
}


This version of the I18N class takes advantage of deferred final static initialization. In the static initializer, you look up the resource bundle by name and initialize it. You then use the bundle to get the values for the strings and initialize them to the proper values. When getting the string values, you add an assert to the getString( ) method, which helps you catch missing strings. Since assertions don't have to be caught—and indeed shouldn't be caught—this won't require you to use a try-catch block in the initializer. Now that the strings are initialized, the user can use them, as shown in this new version of SomeApp:

package oracle.hcj.constants;
public class SomeApp2 {
 public final static void main(final String[] args) {
 System.out.println(I18N2.OK);
 System.out.println(I18N2.CANCEL);
 System.out.println(I18N2.REFRESH);
 // System.out.println(I18N2.SOME_STRING); // <== compiler error
 }
}


The new version is much cleaner and easier to use than the old version. Since the only quoted strings are in the actual initializer for the I18N2 class, any typos will be located there as well as fixed there. Also, you gain two performance benefits. First, since the resource bundle is scoped to the static initializer, it will be garbage-collected and not take up the extra memory after the initializer exits. Second, accessing a substitution constant is significantly faster than calling a method to perform a lookup in a map. You don't have to create a new variable for the quoted string, call a method, set up a try-catch block, or perform any of the other myriad operations needed to look up constants the old way. One final thing to consider when using substitution constants is that it is safe to change a constant in the I18N2 file without recompiling all of the classes that use the constants. Although the constants are final static strings, they are not replaced at compile time by optimizers because they are initialized using method calls. Therefore, instead of optimizing them out, the compiler will put them in the variable space for the app and use them just like references to any other variable.

      
Comments