2.2 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.
2.2.1 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 oreilly.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 oreilly.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=oreilly.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 oreilly.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=oreilly/hcj/finalstory/FinalVariables.java compile_example
compile_example:
[javac] Compiling 1 source file to C:\dev\hcj\bin
[javac] C:\dev\hcj\src\oreilly\hcj\finalstory\FinalVariables.java:53: cannot assign a value to final variable key
[javac] key = new String("someValue"); // <= compiler error.
[javac] ^
[javac] 1 error
|
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 book.
|
|
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.
2.2.2 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 oreilly.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.
|
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.
2.2.2.1 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 oreilly.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 oreilly.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.
|
No comments:
Post a Comment