Final Variables

While we are on the subject of scoped final variables, you should keep in mind that these variables don't have to be primitives to be useful. Final variables that are scoped and constructed can be used as a powerful tool to solidify code in methods.

Method-Scoped final Variables

Although final variables that appear within methods are a little strange to some people at first, they become quite addictive once you get used to reading them. See Example 2-5.

Example 2-5. Catching mistakes with method-scoped final variables
package oracle.hcj.finalstory;
public class FinalVariables {
 public static String someMethod(final String environmentKey) {
 final String key = "env." + environmentKey;
 System.out.println("Key is: " + key);
 return (System.getProperty(key));
 }
}


In this class, you build a scoped final variable that adds a prefix to the parameter environmentKey. In this case, the final variable is final only within the execution scope, which is different at each execution of the method. Each time the method is entered, the final is reconstructed. As soon as it is constructed, it cannot be changed during the scope of the method execution. This allows you to fix a variable in a method for the duration of the method. To see how this works, use the test program in Example 2-6.

Example 2-6. Testing final variables
package oracle.hcj.finalstory;
public class FinalVariables {
 public final static void main(final String[] args) {
 System.out.println("Note how the key variable is changed.");
 someMethod("JAVA_HOME");
 someMethod("ANT_HOME");
 }
}


Running this test program results in the following:

>ant -Dexample=oracle.hcj.finalstory.FinalVariables run_example run_example:
 [java] Note how the key variable is changed.
 [java] Key is: env.JAVA_HOME
 [java] Key is: env.ANT_HOME


Each time the method is entered, the passed-in environmentKey parameter is appended to the constant prefix and then frozen for the duration of the method call. So why make the variable final? Because once this variable is set in the body of the method, it cannot be changed. Consider what would happen if you made a mistake like the one shown in Example 2-7.

Example 2-7. A coding mistake caught by a final variable
package oracle.hcj.finalstory;
public class FinalVariables {
 public static String someBuggedMethod(final String environmentKey) {
 final String key = "env." + environmentKey;
 System.out.println("Key is: " + key);
 key = new String("someValue"); // <= compiler error. 
 return (System.getProperty(key));
 }
}


When you try to compile this code, it will give the following result:

>ant -Dexample=oracle/hcj/finalstory/FinalVariables.java compile_example compile_example:
 [javac] Compiling 1 source file to C:\dev\hcj\bin
 [javac] C:\dev\hcj\src\oracle\hcj\finalstory\FinalVariables.java:53: cannot assign a value to final variable key
 [javac] key = new String("someValue"); // <= compiler error.
 [javac] ^
 [javac] 1 error


Screenshot

In the example code, I commented out the compiler error; you will have to uncomment it to run this test. I use a similar procedure for all compiler errors throughout the tutorial.


In this example, the mistake was made of trying to reassign key to a different value. This type of mistake simply happens; however, since you are a savvy programmer, and you used the final keyword, the compiler tells you that an error was made. This is a great example of trading a logic error for a compiler error. The technique of fixing variables with final is extremely handy for long or complicated methods that have many local variables. When alerted by the compiler, repairing this mistake takes a matter of seconds. If you don't use final to fix your variables now, you run the risk of spending long hours to find logic bugs, only to discover that someone reset your variable halfway through the method because of a typo.

Deferred Initialization

If you want to, you can defer the initialization of a final variable within the method. Hold onto your hat because Example 2-8 is going to look a little weird at first.

Example 2-8. Method-scoped final variables with deferred initialization
package oracle.hcj.finalstory;
public class FinalVariables {
 public void buildGUIDialog (final String name) {
 final String instanceName;
 if (name == null) {
 // no problem here.
 instanceName = getClass( ).getName( ) + hashCode( );
 } else {
 // no problem here as well.
 instanceName = getClass( ).getName( ) + name;
 }
 JDialog dialog = new JDialog( );
 // .. Do a bunch of layout and component building. dialog.setTitle(instanceName); // .. Do dialog assembly
 instanceName = "hello"; // <= compiler error
 }
}


In this case, you declare a final variable at the start of the method without giving it a value, since the contents of that variable depend on whether the user passed you null. During the if statement, check for null and then assign the variable appropriately. Once you assign the variable a value, it can't be assigned a value again. However, you could have gone through half the method before assigning the variable its value. This coding technique allows you to make single-shot, assign-and-freeze variables. After assignment, these variables behave like constants for the rest of the method.

Screenshot

Method-scoped final variables aren't the same as constants, although they behave like constants at times. The difference is that method-scoped final variables are variable. Each time the method is entered, their values are changed based on the needs of that particular execution. However, method-scoped constants always have the same values regardless of the circumstances under which the method is run. Also, primitive and String method-scoped final variables are not substituted at compile time like primitive and String method-scoped constants.


In addition to deferring the initialization of method variables, you can defer the initialization of instance-scoped variables and class-scoped variables. Instance-scoped variables must be initialized in a constructor, and class-scoped variables must be initialized in the static{} method or you will receive compiler errors stating that the variable has not been initialized.

Chained deferred initialization

One interesting trick you can employ with deferred initialization is to chain the initialization of multiple final variables together. For example, consider the following code, which chains instance-scoped final variable initialization:

package oracle.hcj.finalstory;
public class ChainingFinals {
 public final String name;
 public final int nameLength = this.name.length;
 // public final String anotherValue = name; // <== Won't compile
 public ChainingFinals(final String name) {
 this.name = name;
 }
}


In this code, the emphasized line will work properly because the final variable name must be initialized in the constructor to the class. Therefore, the final variable nameLength can take advantage of name when the instance is initialized. However, make sure that you use the this keyword in front of the variable name. If you don't, it won't compile.

package oracle.hcj.finalstory;
public class ChainingFinals {
 public final String name;
 public final int nameLength = name.length; // <== Won't compile
 public ChainingFinals(final String name) {
 this.name = name;
 }
}


In this slightly revised example, I left off the keyword this when using name. As a result, the line won't compile but will instead tell me that name is not declared. The compiler requires that you use the this reference when chaining finals in this manner. Chaining final initialization can be a great tool for precaching data or initializing final members that are dependent on other final members. It also has the benefit of giving you insight into how object instantiation is managed in the virtual machine. As a result of this process, the constructor to the class is run before the initializers.

      
Comments