The
final keyword
The
final
keyword has slightly different meanings depending on the context, but in
general it says “This cannot be changed.” You might want to prevent
changes for two reasons: design or efficiency. Because these two reasons are
quite different, it’s possible to misuse the
final
keyword.
The
following sections discuss the three places where
final
can be used: for data, methods and for a class.
Final
data
Many
programming languages have a way to tell the compiler that a piece of data is
“constant.” A constant is useful for two reasons:
- It
can be a compile-time
constant
that won’t ever change.
- It
can be a value initialized at run-time
that you don’t want changed.
In
the case of a compile-time constant the compiler is allowed to
“fold” the constant
value into any calculations in which it’s used; that is, the calculation
can be performed at compile time, eliminating some run-time overhead. In Java,
these sorts of constants must be primitives
and are expressed using the
final
keyword. A value must be given at the time of definition of such a constant.
A
field that is both static
and
final
has only one piece of storage that cannot be changed.
When
using final
with object handles rather than primitives the meaning gets a bit confusing.
With a primitive,
final
makes the
value
a constant, but with an object handle,
final
makes the handle a constant. The handle must be initialized to an object at the
point of declaration, and the handle can never be changed to point to another
object. However, the object can be modified; Java does not provide a way to
make any arbitrary object a constant. (You can, however, write your class so
that objects have the effect of being constant.) This restriction includes
arrays, which are also objects.
Here’s
an example that demonstrates
final
fields:
//: FinalData.java
// The effect of final on fields
class Value {
int i = 1;
}
public class FinalData {
// Can be compile-time constants
final int i1 = 9;
static final int I2 = 99;
// Typical public constant:
public static final int I3 = 39;
// Cannot be compile-time constants:
final int i4 = (int)(Math.random()*20);
static final int i5 = (int)(Math.random()*20);
Value v1 = new Value();
final Value v2 = new Value();
static final Value v3 = new Value();
//! final Value v4; // Pre-Java 1.1 Error:
// no initializer
// Arrays:
final int[] a = { 1, 2, 3, 4, 5, 6 };
public void print(String id) {
System.out.println(
id + ": " + "i4 = " + i4 +
", i5 = " + i5);
}
public static void main(String[] args) {
FinalData fd1 = new FinalData();
//! fd1.i1++; // Error: can't change value
fd1.v2.i++; // Object isn't constant!
fd1.v1 = new Value(); // OK -- not final
for(int i = 0; i < fd1.a.length; i++)
fd1.a[i]++; // Object isn't constant!
//! fd1.v2 = new Value(); // Error: Can't
//! fd1.v3 = new Value(); // change handle
//! fd1.a = new int[3];
fd1.print("fd1");
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData();
fd1.print("fd1");
fd2.print("fd2");
}
} ///:~
Since
i1
and
I2
are
final
primitives with compile-time values, they can both be used as compile-time
constants and are not different in any important way.
I3
is the more typical way you’ll see such constants defined:
public
so they’re usable outside the package,
static
to emphasize that there’s only one, and
final
to say that it’s a constant. Note that final
static
primitives with constant initial values (that is, compile-time constants) are
named with all capitals by convention. Also note that
i5
cannot be known at compile time, so it is not capitalized.
Just
because something is
final
doesn’t mean that its value is known at compile-time. This is
demonstrated by initializing
i4
and
i5
at run-time using randomly generated numbers. This portion of the example also
shows the difference between making a
final
value
static
or non-
static.
This difference shows up only when the values are initialized at run-time,
since the compile-time values are treated the same by the compiler. (And
presumably optimized out of existence.) The difference is shown in the output
from one run:
fd1: i4 = 15, i5 = 9
Creating new FinalData
fd1: i4 = 15, i5 = 9
fd2: i4 = 10, i5 = 9
Note
that the values of
i4
for
fd1
and
fd2
are unique, but the value for
i5
is not changed by creating the second
FinalData
object. That’s because it’s
static
and is initialized once upon loading and not each time a new object is created.
The
variables
v1
through
v4
demonstrate the meaning of a
final
handle. As you can see in
main( ),
just because
v2
is
final
doesn’t mean that you can’t change its value. However, you cannot
re-bind
v2
to a new object, precisely because it’s
final.
That’s what
final
means for a handle. You can also see the same meaning holds true for an array,
which is just another kind of handle. (There is know way that I know of to make
the array handles themselves
final.)
Making handles
final
seems less useful than making primitives
final.
Blank
finals
Java
1.1
allows the creation of blank
finals
,
which are fields that are declared as
final
but are not given an initialization value. In all cases, the blank final
must
be initialized before it is used, and the compiler ensures this. However, blank
finals provide much more flexibility in the use of the
final
keyword since, for example, a
final
field inside a class can now be different for each object and yet it retains
its immutable quality. Here’s an example:
//: BlankFinal.java
// "Blank" final data members
class Poppet { }
class BlankFinal {
final int i = 0; // Initialized final
final int j; // Blank final
final Poppet p; // Blank final handle
// Blank finals MUST be initialized
// in the constructor:
BlankFinal() {
j = 1; // Initialize blank final
p = new Poppet();
}
BlankFinal(int x) {
j = x; // Initialize blank final
p = new Poppet();
}
public static void main(String[] args) {
BlankFinal bf = new BlankFinal();
}
} ///:~
You’re
forced to perform assignments to finals either with an expression at the point
of definition of the field or in every constructor. This way it’s
guaranteed that the final field is always initialized before use.
Final
arguments
Java
1.1
allows you to make arguments
final
by declaring them as such in the argument list. This means that inside the
method you cannot change what the argument handle points to:
//: FinalArguments.java
// Using "final" with method arguments
class Gizmo {
public void spin() {}
}
public class FinalArguments {
void with(final Gizmo g) {
//! g = new Gizmo(); // Illegal -- g is final
g.spin();
}
void without(Gizmo g) {
g = new Gizmo(); // OK -- g not final
g.spin();
}
// void f(final int i) { i++; } // Can't change
// You can only read from a final primitive:
int g(final int i) { return i + 1; }
public static void main(String[] args) {
FinalArguments bf = new FinalArguments();
bf.without(null);
bf.with(null);
}
} ///:~
Note
that you can still assign a
null
handle to an argument that’s final without the compiler catching it, just
like you can with a non-final argument.
The
methods
f( )
and
g( )
show what happens when primitive arguments are
final:
you can only read the argument, but you can't change it.
Final
methods
There
are two reasons for final
methods. The first is to put a “lock” on the method to prevent any
inheriting class from changing its meaning. This is done for design reasons
when you want to make sure that a method’s behavior is retained during
inheritance and cannot be overridden.
The
second reason for
final
methods is efficiency. If you make a method
final,
you are allowing the compiler to turn any calls to that method into inline
calls. When the compiler sees a
final
method call it can (at its discretion) skip the normal approach of inserting
code to perform the method call mechanism (push arguments on the stack, hop
over to the method code and execute it, hop back and clean off the stack
arguments, and deal with the return value) and instead replace the method call
with a copy of the actual code in the method body. This eliminates the overhead
of the method call. Of course, if a method is big, then your code begins to
bloat and you probably won’t see any performance gains from inlining
since any improvements will be dwarfed by the amount of time spent inside the
method. It is implied that the Java compiler is able to detect these situations
and choose wisely whether to inline a
final
method. However, it’s better to not trust that the compiler is able to do
this and make a method
final
only if it’s quite small or if you want to explicitly prevent overriding.
Any
private
methods in a class are implicitly
final.
Because you can’t access a
private
method,
you can’t override it (the compiler gives an error message if you try).
You can add the
final
specifier to a
private
method but it doesn’t give that method any extra meaning.
Final
classes
When
you say that an entire class is
final
(by preceding its definition with the
final
keyword), you state that you don’t want to inherit from this class or
allow anyone else to do so. In other words, for some reason the design of your
class is such that there is never a need to make any changes, or for safety or
security reasons you don’t want subclassing. Alternatively, you might be
dealing with an efficiency issue and you want to make sure that any activity
involved with objects of this class is as efficient as possible.
//: Jurassic.java
// Making an entire class final
class SmallBrain {}
final class Dinosaur {
int i = 7;
int j = 1;
SmallBrain x = new SmallBrain();
void f() {}
}
//! class Further extends Dinosaur {}
// error: Cannot extend final class 'Dinosaur'
public class Jurassic {
public static void main(String[] args) {
Dinosaur n = new Dinosaur();
n.f();
n.i = 40;
n.j++;
}
} ///:~
Note
that the data members can be
final
or not, as you choose. The same rules apply to
final
for data members regardless of whether the class is defined as
final.
Defining the class as
final
simply prevents inheritance – nothing more. However, because it prevents inheritance
all methods in a
final
class are implicitly
final,
since there’s no way to override them. So the compiler has the same
efficiency options as it does if you explicitly declare a method
final. You
can add the
final
specifier to a method in a
final
class, but it doesn’t add any meaning.
Final
caution
It
can seem to be sensible to make a method
final
while you’re designing a class. You might feel that efficiency
is very important when using your class and that no one could possibly want to
override your methods anyway. Sometimes this is true.
But
be careful with your assumptions. In general, it’s difficult to
anticipate how a class can be reused, especially a general-purpose class. If
you define a method as
final
you might prevent the possibility of reusing your class through inheritance in
some other programmer’s project simply because you couldn’t imagine
it being used that way.
The
standard Java library is a good example of this. In particular, the
Vector
class is commonly used and might be even more useful if, in the name of
efficiency, all the methods hadn’t been made
final.
It’s easily conceivable that you might want to inherit and override with
such a fundamentally useful class, but the designers somehow decided this
wasn’t appropriate. This is ironic for two reasons. First,
Stack
is
inherited from
Vector,
which says that a
Stack
is
a
Vector,
which isn’t really true. Second, many of the most important methods of
Vector,
such as
addElement( )
and
elementAt( )
are
synchronized,
which as you will see in Chapter 14 incurs a significant performance overhead
that probably wipes out any gains provided by
final.
This lends credence to the theory that programmers are consistently bad at
guessing where optimizations should occur. It’s just too bad that such a
clumsy design made it into the standard library where we must all cope with it.
It’s
also interesting to note that
Hashtable,
another important standard library class, does
not
have any
final
methods. As mentioned elsewhere in this book, it’s quite obvious that
some classes were designed by completely different people than others. (Notice
the brevity of the method names in
Hashtable
compared to those in
Vector.)
This is precisely the sort of thing that should
not
be obvious to consumers of a class library. When things are inconsistent it
just makes more work for the user. Yet another paean to the value of design and
code walkthroughs.