The final keyword

Bruce Eckel’s Thinking in Java Contents | Prev | Next

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:

  1. It
    can be a
    compile-time
    constant

    that won’t ever change.
  2. It
    can be a value initialized at
    run-time
    that you don’t want changed.
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

//: 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

//: 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

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.

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.

Final
caution

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.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read