Sponsored Link •
|
Advertisement
|
Variables have lifetimes. The lifetime of an instance variable matches that of the object to which it belongs. The lifetime of a class variable matches that of the class to which it belongs. The lifetime of a local variable is from the point it is created to the point where it goes out of scope. The Java language and virtual machine have mechanisms to ensure each of these kinds of variables are initialized before they are used.
As discussed in Chapter 3, the Java compiler and Java virtual machine make sure local variables are explicitly initialized before they are used.
[bv: talk briefly about the lifetime of a class here]
At the beginning of an object's life, the Java virtual machine (JVM) allocates enough memory on the heap to accommodate the object's instance variables. When that memory is first allocated, however, the data it contains is unpredictable. If the memory were used as is, the behavior of the object would also be unpredictable. To guard against such a scenario, Java makes certain that memory is initialized, at least to predictable default values, before it is used by any code.
Initialization is important because, historically, uninitialized data has been a common source of bugs. Bugs caused by uninitialized data occur regularly in C, for example, because it doesn't have built-in mechanisms to enforce proper initialization of data. C programmers must always remember to initialize data after they allocate it and before they use it. The Java language, by contrast, has built-in mechanisms that help you ensure proper initialization of the memory occupied by a newly-created object. With proper use of these mechanisms, you can prevent an object of your design from ever being created with an invalid initial state.
The Java language has three mechanisms dedicated to ensuring proper initialization of objects:
new
operator,
the Java virtual machine will insure that initialization code is run before
you can use the newly-allocated memory. If you design your classes such
that initializers and constructors always produce a valid state for
newly-created objects, there will be no way for anyone to create and
use an object that isn't properly initialized.
Up to this point, the examples used in this book have done no explicit initialization of instance variables. This is perfectly legal in Java, and results in predictable default initial values, which are based only upon the type of the variable. Table 4-1 shows the default initial values for each of the variable types. (These are the default initial values for both instance and class variables. Initialization of class variables will be discussed in depth in Chapter 6.)
Type | Default Value |
---|---|
boolean |
false |
byte |
(byte) 0 |
short |
(short) 0 |
int |
0 |
long |
0L |
char |
\u0000 |
float |
0.0f |
double |
0.0d |
object reference | null |
Table 4-1. Default values for fields
If you don't explicitly initialize an instance variable, then that variable will still have its default initial
value when new
returns its reference. For example, in all the versions of class
CoffeeCup
prior to this chapter, the innerCoffee
field was
not explicitly initialized (The class contains no constructors or initializers.):
// This class has no constructors or initializers public class CoffeeCup { private int innerCoffee; // The rest of the class... }As a result, when the reference to a new
CoffeeCup
object is first returned by
new
, the innerCoffee
field would be its default initial value.
Because innerCoffee
is an int
, its default initial value is zero.
Note that this means that if you explicitly initialize
innerCoffee
, say to a value of 100, then when each
CoffeeCup
object is created, innerCoffee
will, in effect, be initialized twice. First, innerCoffee
will be given its default initial value of zero. Later, the zero will
be overwritten with the proper initial value of 100. All of this takes
place while the Java virtual machine is creating the new object --
before it returns the reference to the new object. By the time the
reference to a new CoffeeCup
object is returned from the
new
operator, the innerCoffee
field will be
set to 100.
As mentioned in Chapter 2, local variables do not participate in the default initial values that instance variables (and, as you will see in Chapter 6, class variables) are guaranteed to receive. The value of a local variable is undefined until you explicitly initialize it.
The central player in object initialization is the constructor. In Java, constructors are similar to methods, but they are not methods. Like a method, a constructor has a set of parameters and a body of code. Unlike methods, however, constructors have no return type. Like methods, you can give access specifiers to constructors, but unlike methods, constructors with public, protected, or package access are not inherited by subclasses. (Also, instead of determining the ability to invoke a method, the access level of a constructor determines the ability to instantiate an object.)
In the source file, a constructor looks like a method declaration in
which the method has the same name as the class but has no return type.
For example, here is a constructor declaration for class
CoffeeCup
:
// In source packet in file init/ex2/CoffeeCup.java class CoffeeCup { // Constructor looks like a method declaration // minus the return type public CoffeeCup() { // Body of constructor } // ... }
As with methods, you can overload constructors by varying the number,
types, and order of parameters. Here is a class CoffeeCup
with two constructors:
// In source packet in file init/ex3/CoffeeCup.java class CoffeeCup { private int innerCoffee; public CoffeeCup() { innerCoffee = 237; } public CoffeeCup(int amount) { innerCoffee = amount; } // ... }
When you instantiate an object with new
, you must specify
a constructor. For example, given the CoffeeCup
class
above that has two constructors, you could instantiate it in either of
these two ways:
// In source packet in file init/ex3/Example3.java class Example3 { public static void main(String[] args) { // Create an empty cup CoffeeCup cup1 = new CoffeeCup(); // Create a cup with 355 ml of coffee in it CoffeeCup cup2 = new CoffeeCup(355); } }
In Java jargon, constructors that take no parameters (or no arguments)
are called "no-arg constructors." In the code shown above,
the first instantiation of a CoffeeCup
object specifies
the no-arg constructor. The second instantiation specifies the
constructor that requires an int
as its only parameter.
this()
invocation
From within a constructor, you can explicitly invoke another
constructor from the same class by using the
this()
statement. You may want to do this if you have
several overloaded constructors in a class, all of which must execute
much of the same code. Here's an example:
// In source packet in file init/ex4/CoffeeCup.java class CoffeeCup { private int innerCoffee; public CoffeeCup() { this(237); // Calls other constructor // Could have done more construction here } public CoffeeCup(int amount) { innerCoffee = amount; } // ... }
In this example, the no-arg constructor invokes the constructor
that takes an int
as its only parameter. It passes
237
to the other constructor, which assigns that value to
innerCoffee
.
You cannot call this()
from methods, only from
constructors. If you do call this()
in a constructor, you
must call it first, before any other code in the constructor, and you
can only call it once. Any code you include after the call to
this()
will be executed after the invoked constructor
completes.
Constructors Are Not Methods
To further illustrate the difference between methods and constructors,
consider this fact: The name of a class is a valid name for its
methods. In other words, class CoffeeCup
could have
methods named CoffeeCup
:
// In source packet in file init/ex5/CoffeeCup.java // THIS WORKS, BUT IT IS AN EXAMPLE OF POOR METHOD NAMING class CoffeeCup { private int innerCoffee; public CoffeeCup() { // The constructor innerCoffee = 237; } public void CoffeeCup() { // The method innerCoffee = 99; } // ... }
Given the above definition of class CoffeeCup
, you could
legally do the following:
// In source packet in file init/ex5/Example5.java class Example5 { public static void main(String[] args) { CoffeeCup cup = new CoffeeCup(); // invoke the constructor cup.CoffeeCup(); // invoke the method } }
Although it is legal to give a method the same name as a class, in
practice you should never do so, in part because other programmers
might confuse it with a constructor, but also because it breaks many of
the rules for good method naming. First, a class name is not a verb;
it's a noun (at least it should be a noun). Method names
should be verbs. You should name methods after the action they perform,
and "CoffeeCup
" is not an action. Also,
"CoffeeCup
" doesn't follow the naming convention
for methods, in which the first letter is lowercase. The purpose of this
example is merely to highlight the fact that constructors aren't
methods by showing that a constructor does not conflict with a method
that has the same signature. Java's recommended naming conventions are
described later in this chapter, in the Design Corner section.
If you declare a class with no constructors, the compiler will generate a constructor for you. Such automatically-generated constructors, which are called default constructors, take no parameters (they are no-arg constructors) and have empty bodies. Because the compiler will automatically generate a default constructor if you don't declare any constructors explicitly, all classes are guaranteed to have at least one constructor.
For
example, if you declare a CoffeeCup
class without
declaring a constructor explicitly:
// In source packet in file init/ex6/CoffeeCup.java class CoffeeCup { private int innerCoffee; public void add(int amount) { innerCoffee += amount; } //... }
The compiler will generate the same class file as if you had explicitly declared a no-arg constructor with an empty body:
// In source packet in file init/ex7/CoffeeCup.java class CoffeeCup { private int innerCoffee; public CoffeeCup() { } public void add(int amount) { innerCoffee += amount; } //... }
When you compile a class, the Java compiler creates an instance
initialization method for each constructor you declare in the
source code of the class. Although the constructor is not a method, the
instance initialization method is. It has a name, <init>
, a return type,
void
, and a set of parameters that match the parameters of
the constructor from which it was generated. For example, given the following
two constructors in the source file for class CoffeeCup
:
// In source packet in file init/ex8/CoffeeCup.java class CoffeeCup { public CoffeeCup() { //... } public CoffeeCup(int amount) { //... } // ... }
the compiler would generate the following two instance initialization
methods in the class file for class CoffeeCup
, one for
each constructor in the source file:
// In binary form in file init/ex8/CoffeeCup.class: public void <init>(CoffeeCup this) {...} public void <init>(CoffeeCup this, int amount) {...}
Note that <init>
is not a valid
Java method name, so you could not define a method in your source file
that accidentally conflicted with an instance initialization method.
(To be precise, <init>
is not a method in the Java
language sense of the term, because it has an illegal name. In the
compiled, binary Java class file, however, it is a valid method.)
Also, the this
reference passed as the first parameter to
<init>
is inserted by the Java compiler into the
parameter list of every instance method. For example, the method
void add(int amount)
in the source file for
class CoffeeCup
would become the void add(CoffeeCup
this, int amount)
method in the class file. The hidden
this
reference is the way in which instance methods,
including instance initialization methods, are able to access instance
data.
If you don't explicitly declare a constructor in a class, the Java compiler will create a default constructor on the fly, then translate that default constructor into a corresponding instance initialization method. Thus, every class will have at least one instance initialization method.
When the compiler generates an instance initialization method, it bases
it on a constructor. It gives the method the same parameter list as the
constructor, and it puts the code contained in the constructor's body
into the method's body. But the instance initialization method does not
necessarily represent a mere compilation of the constructor with the
name changed to <init>
and a return value of
void
added. Often, the code of an instance initialization
method does more than the code defined in the body of its corresponding
constructor. The compiler also potentially adds code for any
initializers and an invocation of the superclass's constructor.
Besides constructors, Java offers one other way for you to assign an initial value to instance variables: initializers. As mentioned previously, the two kinds of initializers in Java are instance variable initializers and instance initializers.
In a constructor, you have the freedom to write as much code as needed to calculate an initial value. In an instance variable initializer, by contrast, you have only an equals sign and an expression. The left-hand side of the equals sign is the instance variable being initialized. The right-hand side of the equals sign can be any expression that evaluates to the type of the instance variable.
For example, if you wanted
to always start coffee cups out with 355 milliliters of fresh brewed
coffee in them, you could initialize innerCoffee
with a
constructor:
// In source packet in file init/ex9/CoffeeCup.java class CoffeeCup { private int innerCoffee; public CoffeeCup() { innerCoffee = 355; } // ... }
Alternatively, you could initialize innerCoffee
with an
instance variable initializer:
// In source packet in file init/ex10/CoffeeCup.java class CoffeeCup { private int innerCoffee = 355; // "= 355" is an initializer // no constructor here // ... }
Java 1.1 introduced instance initializers. Instance initializers, which may also be called instance initialization blocks, are blocks of code (marked by open and close curly braces) that sit in the body of a class, but outside the body of any method declared in that class.
For example, here is the same
CoffeeCup
class with its innerCoffee
variable initialized
by an instance initializer:
// In source packet in file init/ex19/CoffeeCup.java class CoffeeCup { private int innerCoffee; // The following block is an instance initializer { innerCoffee = 355; } // no constructor here // ... }
This manner of initializing innerCoffee
yields the same
result as the previous two examples: innerCoffee
is
initialized to 355.
Instance initializers are a useful alternative to instance variable initializers whenever: (1) initializer code must catch exceptions (described in Chapter 13), or (2) perform fancy calculations that can't be expressed with an instance variable initializer. You could, of course, always write such code in constructors. But in a class that had multiple constructors, you would have to repeat the code in each constructor. With an instance initializer, you can just write the code once, and it will be executed no matter what constructor is used to create the object. Instance initializers are also useful in anonymous inner classes (described in Chapter 11), which can't declare any constructors at all.
Note that the code inside an instance initializer may not return. In addition, instance initializers have special rules regarding exceptions. Information about these special rules is given in Chapter 13.
When you write an initializer (either an instance variable initializer or instance initializer), you must be sure not to refer to any instance variables declared textually after the variable being initialized. In other words, you can't make a forward reference from an initializer. (A forward reference is simply a use of a variable declared textually after the current statement in the source file.) If you disobey this rule, the compiler will give you an error message and refuse to generate a class file.
When an object is created, initializers are executed in textual order -- their order of appearance in the source code. The forward-referencing rule helps prevent initializers from using instance variables that have yet to be properly initialized.
For example, here is a virtual cafe class that has four chairs for every table:
// In source packet in file init/ex11/VirtualCafe.java class VirtualCafe { private int tablesCount = 20; private int chairsCount = 4 * tablesCount; //... }
These initializers work fine. The chairsCount
initializer,
= 4 * tablesCount
, refers to an instance variable declared
textually before it, so the compiler is happy. Because initializers
are executed in textual order, tablesCount
is already
initialized to 20 by the time chairsCount
's initializer
multiplies it by four. Thus, chairsCount
is initialized to
80.
If you were able to use instance variables declared textually later, you could end up with unexpected behavior:
// In source packet in file init/ex12/VirtualCafe.java // THIS WON'T COMPILE, BUT AS A THOUGHT EXPERIMENT, // IMAGINE IT WERE POSSIBLE class VirtualCafe { private int chairsCount = 4 * tablesCount; private int tablesCount = 20; //... }
If the above declaration were possible, chairsCount
's
initializer would use tablesCount
before
tablesCount
were assigned a value of 20. At that point,
the tablesCount
variable would have its default initial
value of zero. Hence, this code would initialize
chairsCount
to four times zero. If you do the math, you
will discover that, in this case, chairsCount
does not
get initialized to 80.
Although this kind of forward referencing is disallowed by the compiler in an attempt to help programmers avoid just the above kind of mistake, you can't let down your guard completely. There is still a way you could inadvertently (or purposefully) circumvent the compiler's preventative restrictions:
// In source packet in file init/ex13/VirtualCafe.java class VirtualCafe { private int chairsCount = initChairsCount(); private int tablesCount = 20; private int initChairsCount() { return tablesCount * 4; } //... }
The above code compiles fine, and has the same result as the previous
thought experiment. Here chairsCount
's initializer
sneakily invokes a method that uses tablesCount
before its
initializer has been executed. When initChairsCount()
calculates tablesCount * 4
, tablesCount
is
still at its default initial value of zero. As a result,
initChairsCount()
returns zero, and chairsCount
is initialized to zero.
new
operator, Java provides three other ways
to instantiate objects:
clone()
on an object
newInstance()
method of class java.lang.Class
newInstance()
in Chapter 23.
Although the information here may seem like a lot, there is still more to object initialization, in particular, the way in which initialization interacts with inheritence. Information about initialization and inheritance will be given in Chapter 7.
[bv: perhaps show that diagram of relationship between language,
class file, and virtual machine.]
The <init>
method is not actually part of the Java
language. Rather, it is something the Java virtual machine expects to
see in a Java class file. This distinction is significant because the
Java language does not depend on the class file. Java source can be
compiled into other binary formats, including native executables. A
Java compiler that translates Java language source into some other
binary format need not generate a method named
<init>
, so long as objects are initialized in the
proper way at the proper time. The Java Language Specification (JLS) details
the order of initialization and when it occurs, but doesn't say how it
is actually accomplished.
Still,
understanding how initialization works inside class files can help you
understand the order of initialization in the language.
Variables have lifetimes. The lifetime of an instance variable matches that of the object to which it belongs. The lifetime of a class variable matches that of the class to which it belongs. The lifetime of a local variable is from the point it is created to the point where it goes out of scope. The Java language and virtual machine have mechanisms to ensure each of these kinds of variables are initialized before they are used.
[bv: talk briefly about the lifetime of a class here] [bv:Clean up this class init stuff. It is too redundant with object init stuff and a bit too cutseypie with suger packets.]
Introduce class variable initializers and static initialization blocks.
Class variables get initialized to the same default initial values as instance variables. These default initial values are shown in Table 4-1.
public class SugarHolder { private int sugarPacketCount; // instance variable private static int sugarPacketsInCafeCount; // class variable }
Class SugarHolder
contains one instance variable and one class variable. In
this example, the SugarHolder
class maintains a class variable,
sugarPacketsInCafeCount
, to track the total number of packets of sugar sitting
in sugar holders throughout the cafe. The instance variable, sugarPacketCount
,
just tracks the number of packets of sugar in an individual sugar holder.
In the example above, sugarPacketsInCafeCount
will have an initial value
of zero, because zero is the default initial value for ints
. If you wanted a different
initial value for sugarPacketsInCafeCount
, you would have to explicitly
initialize it to the other value. For example, imagine you are simulating a cafe that always starts out with
five sugar holders, each of which contains five sugar packets. You could easily initialize the
sugarPacketsInCafeCount
to 25 using a class variable initializer.
You write a class variable initializer with an equals sign and an expression, as shown below:
class SugarHolder { private int sugarPacketCount; private static int sugarPacketsInCafeCount = 25; }
In the above example, sugarPacketsInCafeCount
will be set to 25 before
any use of the class. The class variable initializer is " = 25".
The Java compiler enforces the same forward referencing rule on class variable initializers. Class variable initializers may refer to any class variable that appears textually before it in the class, but not after. This rule aims to prevent situations in which a class variable is used before it is initialized.
Some examples of static initialization blocks are shown below. In these examples the
SugarHolder
class from an earlier example has been enhanced to include class
variables that track the total number of particular types of sugar packets in the cafe. This cafe offers four
type of sugar packets, each of which comes in a different color package. The white packet contains plain
old sugar. The brown packet contains unbleached brown granules of sugar. The pink and blue packets
contain two different brands of sugar substitutes. The first example shows an unruly class initializer that
refers to class variables that appear textually after it. Because of this one class variable initializer's
disregard for the rules, the entire class will be rejected by the compiler.
class SugarHolder { private int sugarPacketCount; // This won't compile because the following class // variable initializer refers to class variables // that appear textually after it. private static int sugarPacketsInCafeCount = whitePacketsInCafeCount + brownPacketsInCafeCount + pinkPacketsInCafeCount + bluePacketsInCafeCount; private static int whitePacketsInCafeCount = 10; private static int brownPacketsInCafeCount = 5; private static int pinkPacketsInCafeCount = 5; private static int bluePacketsInCafeCount = 5; }
The following example rectifies the problem by simply moving the declaration of
sugarPacketsInCafeCount
such that it appears textually after the declarations
of the class variables it uses.
class SugarHolder { private int sugarPacketCount; private static int whitePacketsInCafeCount = 10; private static int brownPacketsInCafeCount = 5; private static int pinkPacketsInCafeCount = 5; private static int bluePacketsInCafeCount = 5; // This class will compile because the following class // variable initializer only refers to other class // variables that appear textually before it. private static int sugarPacketsInCafeCount = whitePacketsInCafeCount + brownPacketsInCafeCount + pinkPacketsInCafeCount + bluePacketsInCafeCount; }
If you need to initialize a class variable in a manner too complex to express with an equals sign and
expression, you can write a static initialization block. A static initialization block is a code
block preceded by the word static
, as in:
class SugarHolder { // ... static { // code for the static initialization // goes here } // ...
For a more concrete example, suppose instead of starting over with a fresh cafe each time you run your simulation program, you want to continue the state of the previous cafe. In this case you could write a static initialization block such as the one shown below:
// Defined in file SugarHolder.java public class SugarHolder { private int sugarPacketCount; private static int sugarPacketsInCafeCount; static { // Initialize the total number of sugar packets with // state data left over from previous execution of // the simulation program. int sugarHolderCount = PersistentStorage.getSugarHolderCount(); for (int i = 0; i < sugarHolderCount; ++i) { sugarPacketsInCafeCount += PersistentStorage.getSugarPacketCount(i); } } } // Defined in file PersistentStorage.java: public class PersistentStorage { public int getSugarPacketCount(int sugarHolderIndex) { // For now return something that looks good. Later, // actually set up persistent storage return 23; } // ... }
In the above example, the code in the static initialization block will be executed before the class is
used, resulting in a sugarPacketsInCafeCount
that is initialized to a value
retrieved from persistent storage.
You can include in a class as many class variable initializers as there are class variables, and as many static initialization blocks as you wish. You can place static initialization blocks anywhere in your class. You could, for example, write one static initialization block that initializes several class variables. Alternatively, you could write a separate static initialization block for each class variable. You could even write several static initialization blocks for a single class variable, although in this case your peers might think you're a bit odd. The point here is that there is no one-to-one correspondence between class variables and static initialization blocks. You can, in fact, create static initialization blocks in classes that don't have any class variables.
Static initialization blocks have full privileges of regular static methods, with one exception. A static initialization block may refer to any class variable that appears textually before it in the class, but not after. The Java compiler will balk if one of your static initialization blocks uses or assigns a class variable the appears textually after it, even though such variables are in scope.
The next example includes a static initialization block with a healthy disrespect for authority. Like the irreverent class variable initializer from a previous example, this static initialization block accesses class variables that appear textually after it. In this case, authority will win again, because the class will not compile.
// Defined in file SugarHolder.java: // This class won't compile, because the static initialization block // refers to class variables that appear textually after it. class SugarHolder { private int sugarPacketCount; private static int sugarPacketsInCafeCount; static { // Initialize the total number of sugar packets with // state data left over from previous execution of // the simulation program. int sugarHolderCount = PersistentStorage.getSugarHolderCount(); for (int i = 0; i < sugarHolderCount; ++i) { sugarPacketsInCafeCount += PersistentStorage.getSugarPacketCount(i); whitePacketsInCafeCount += PersistentStorage.getWhitePacketCount(i); brownPacketsInCafeCount += PersistentStorage.getBrownPacketCount(i); pinkPacketsInCafeCount += PersistentStorage.getPinkPacketCount(i); bluePacketsInCafeCount += PersistentStorage.getBluePacketCount(i); } } private static int whitePacketsInCafeCount; private static int brownPacketsInCafeCount; private static int pinkPacketsInCafeCount; private static int bluePacketsInCafeCount; } // Defined in file PersistentStorage.java: public class PersistentStorage { public int getSugarPacketCount(int sugarHolderIndex) { return 23; } public int getWhitePacketCount(int sugarHolderIndex) { return 6; } public int getBrownPacketCount(int sugarHolderIndex) { return 7; } public int getPinkPacketCount(int sugarHolderIndex) { return 5; } public int getBluePacketCount(int sugarHolderIndex) { return 5; } // ... }
One solution to the problem in the previous example is to simply move the colored packet class variables above the static initialization block, as shown below:
// This class compiles happily. class SugarHolder { private int sugarPacketCount; private static int sugarPacketsInCafeCount; private static int whitePacketsInCafeCount; private static int brownPacketsInCafeCount; private static int pinkPacketsInCafeCount; private static int bluePacketsInCafeCount; static { // Initialize the total number of sugar packets with // state data left over from previous execution of // the simulation program. int sugarHolderCount = PersistentStorage.getSugarHolderCount(); for (int i = 0; i < sugarHolderCount; ++i) { sugarPacketsInCafeCount += PersistentStorage.getSugarPacketCount(i); whitePacketsInCafeCount += PersistentStorage.getWhitePacketCount(i); brownPacketsInCafeCount += PersistentStorage.getBrownPacketCount(i); pinkPacketsInCafeCount += PersistentStorage.getPinkPacketCount(i); bluePacketsInCafeCount += PersistentStorage.getBluePacketCount(i); } } }
Another solution is to move the problem-causing portion of the static initialization block code to another static initialization block that appears textually after the colored packet class variables, as in:
// This class also compiles happily. class SugarHolder { private int sugarPacketCount; private static int sugarPacketsInCafeCount; static { // Initialize the total number of sugar packets with // state data left over from previous execution of // the simulation program. int sugarHolderCount = PersistentStorage.getSugarHolderCount(); for (int i = 0; i < sugarHolderCount; ++i) { sugarPacketsInCafeCount += PersistentStorage.getSugarPacketCount(i); } } private static int whitePacketsInCafeCount; private static int brownPacketsInCafeCount; private static int pinkPacketsInCafeCount; private static int bluePacketsInCafeCount; static { // Initialize the total number of colored packets with // state data left over from previous execution of // the simulation program. int sugarHolderCount = PersistentStorage.getSugarHolderCount(); for (int i = 0; i < sugarHolderCount; ++i) { whitePacketsInCafeCount += PersistentStorage.getWhitePacketCount(i); brownPacketsInCafeCount += PersistentStorage.getBrownPacketCount(i); pinkPacketsInCafeCount += PersistentStorage.getPinkPacketCount(i); bluePacketsInCafeCount += PersistentStorage.getBluePacketCount(i); } } }
[bv: mention things get initialized in textual order?]
As an example of a static initialization block in a class that contains no class variables, consider the
StaticGreeting class below. From outward appearances, this class behaves like a typical "Hello World"
program with a coffee-oriented message. This program, however, manages to print its greeting before the
main
method is ever invoked. Even though the class does not contain any class
variables, the static intialization block will be rolled into a <clinit>
method. The Java Virtual Machine will execute the <clinit>
method when
the class is initialized, before main
is invoked, and the
<clinit>
method will print out the greeting.
class StaticGreeting { public static void main(String args[]) { } static { System.out.println("Wake up and smell the coffee!"); } }
When executed, the above program prints out:
Wake up and smell the coffee!
The example above points out that code within a static initialization block is not limited to just
initializing class variables, but can do anything a regular static
method can do, so
long as it doesn't access class variables that appear textually after it.
Yet as a design guideline, you should try to keep initializers just
focused on initialization.
Code within static initalizers can call other static methods in the class, can instantiate an object of its own type and call instance methods on the object, can call methods in other classes and pass the object to those methods, all before the class has been completely initialized. This means that you have the power to write static initialization blocks such that a class is used before it is fully initialized. As a result some class variables could be used while they still have their default initial values, rather than their proper initial values. For example, the following program demonstrates one way a class can be used before it is initialized:
class StaticGreeting2 { public static void main(String args[]) { } static { printGreeting(); } String s = "Wake up and smell the coffee!"; static void printGreeting() { System.out.println(s); } }
When executed, the above program prints out the following decidedly feeble greeting:
null
In the previous example, the static initialization block invoked static method
printGreeting
before String s
was initialized with a cheery
greeting. In effect, the static initialization block used a loophole in the law against referring to class
variables that appear textually later. It invoked a method that refers to the class variable. In so doing, the
class variable is used while it still had its default initial value of null
. When
designing static initialization blocks, you should attempt to ensure that class variables are not used before
they are fully initialized.
Static init blocks can call static methods. So methods can run even when things haven't had their initializers run. This is true for instance fields too. BUT, because they always have their default value, at least things are predictable.
[bv: refer to exceptions chapter, as in obj init chapter.] Static init blocks can't result in checked exceptions being thrown. They can invoke methods that may throw checked exceptions, but they must catch them. Similar is true for class var initializers.
All the class variable initializers and static initialization blocks of a class are collected by the Java
compiler and placed into one special method, the class initialization method. This method is
named "<clinit>
". Static initialization blocks and class variable
initializers are executed in textual order, because the code of the <clinit>
method implements the class variable initializers and static initialization blocks in the order in which they
appear in the class declaration. If a class has no class variable initializers or static initialization blocks, it
will have no <clinit>
method. The Java Virtual Machine invokes the
<clinit>
method, if it exists, once for each class. Regular methods cannot
invoke a <clinit>
method, because
<clinit>
is not a valid method name in Java. During a class's preparation
phase, the class variables are set to default values. During the initialization phase, the Java Virtual
Machine executes the <clinit>
method, which sets class variables to their
proper initial values.
[bv: mention name spaces]
The Java Virtual Machine initializes classes and interfaces on their first active use. An active use is:
static
and final
, and are initialized by a
compile-time constant expression.
[bv: perhaps give a nice example of this.]
The Java compiler resolves references to fields that are both static
and
final
, and initialized by a compile-time constant expression, to a copy of the
constant value. An example of two constant fields are shown below as part of class
CoffeeCup
. The maxCapacity
field is initialized by a compile-
time constant, but the currentCoffeeAmount
field is not. References to the
maxCapacity
field are compiled to refer directly to a constant pool entry with the
constant value 250. The maxCapacity
field is not initialized by the
<clinit>
method. However, because the constant value of
currentCoffeeAmount
is not known at compile time, it must be initialized by
<clinit>
. The Java Virtual Machine will execute the
<clinit>
method during the initialization phase of the class, and the
<clinit>
method will invoke Math.random
, multiply
the result by 250, and assign currentCoffeeAmount
its initial value, which will
thereafter remain constant.
class CoffeeCup { static final int maxCapacity = 250; // milliliters static final int currentCoffeeAmount = (int) (Math.random() * 250d); }
The use or assignment of a static
, final
field is not an
active use of a class, because such fields are not initialized via the <clinit>
method. A constant field is a class variable that is declared final
as well as
static
, and is initialized with a compile-time constant expression. Such fields are
resolved to constant expressions To initialize a class, the Java Virtual Machine executes the class's
<clinit>
method, if it has one. A class's superclass must be initialized
before the class is initialized, so the Java Virtual Machine will invoke the
<clinit>
methods of the class's superclass if it hasn't yet been invoked.
The code of a <clinit>
method does not explicitly invoke a superclass's
<clinit>
method, therefore the Java Virtual Machine must make certain
the <clinit>
methods of superclasses have been executed before it invokes
the <clinit>
method of a class.
To initialize an interface, the Java Virtual Machine executes the interface's
<clinit>
method, if it has one, just as it does for classes. An interface may
have a <clinit>
method because interfaces can contain constants. In
interfaces, constants are public
, final
,
static
variables. The variables must have an initialization expression, which is the
interface version of a class variable initializer. Interfaces cannot have static initialization blocks. As with
classes, only initializers that require evaluation of expressions that are not compile-time constants will end
up as code in the <clinit>
method. Therefore, if an interface has at least
one constant that is initialized by an expression that is not a compile time constant, that interface will
have a <clinit>
method. Initialization of an interface does not require
prior initialization of its superinterfaces.
[SHOW THAT THESE ARE PUT INTO SEPARATE CLASS FILES AND THAT THEY ARE USED BY THE CASE STATEMENTS OF SWITCHES, AND TO DO CONDITIONAL COMPILATION.]
This chapter shows you how to ensure proper cleanup of objects at the end of their lives. It discusses garbage collection, finalizers, and cleanup routines to release non-memory resources.
Here, "end of an object's lifetime" does not mean when an object is garbage collected, but when your program stops using it. If your object's validity depends upon its exclusive hold on a non-memory resource, such as a file handle, your object should release that resource not upon garbage collection, but upon
Class FileOutputStream is the perfect example of a cleanup method (close()) and the double checking done by a finalizer.
[MENTION freeMemory(), and totalMemory() in Runtime and runFinalization(), gc() in both System and Runtime.]
[A] Cleanup
Cleanup is about releasing resources at the end of an object's life.
I. Cleanup is important because objects use finite resources which must be released back to the system at the end of an object's life. Certainly memory, possibly other resources. Java has a garbage collector to clean up memory. You must write Java code to clean up any non-memory resource, including possibly a special method called a finalizer which is automatically run by the garbage collector when the object is reclaimed.
[B] The Garbage Collector
I. Paint a general picture of the garbage collector
II. Explain when finalize() is called by the garbage collector. Perhaps paint a picture of the garbage collection session.
III. Objects may never get garbage collected. A lot of times garbage collection happens only when the system runs out of memory. If it doesn't ever run out of memory, it doesn't ever garbage collect.
IV. You can give the gc a hint that this would be a good time to run.
V. Finalizers may or may not be run on exit, depending upon the runFinalizersOnExit() parameter of System? Did this get into Java 1.1?
VI. Describe the process of first pass, second pass.
[B] Finalizers
I. Finalizers can help if you allocate memory in native methods.
II. Could use finalizers to release memory created in some other way than by calling new. (For example, in native methods.) "Finalizers are about memory." That's all they should be worrying about.
III. Finalizers can use try/catch to handle any exceptions thrown during their execution. Any uncaught exceptions thrown by the finalizer itself are ignored by the JVM.
IV. Don't resurrect objects. Clone() an existing one if you have to.
V. Finalizer() is called only once by the garbage collector. So if you resurrect it, and then it gets garbage collected again, finalizer() won't get called the second time.
VI. The possibility of object resurrection shows that the gc must do another marking process after the finalizers have been run.
VII. Always invoke super.finalize() at the end of your finalizer().
[B] Why use a finalizer?
1) Not very many compelling reasons. Most of the time, don't.
2) Give an example of using the finalizer to double check on a sloppy programmer who didn't call close() or cleanup(). (Talk about just opening the file when you need it, using it, then closing it all in one method. In other words, the file handle is not part of the object's long term state. In this case, you don't have to worry about close(), cleanup(), or finalize(). But if it takes up too much time to do all that openning and closing...)
3) One of the few things finalizers are good for is to keep track of data about the garbage collector's performance. Another is for freeing memory that was allocated via native methods. Even this, however, should be able to fit into a cleanUp() method.
4) Even though Object() has a protected finalize(), that doesn't mean the the Java Virtual Machine will always call it. Java Virtual Machine implementations should be smart about figuring out whether you actually override it or even do something useful in the overridden guy.
[B] Strategies for ensuring the release of non-memory resources Clean up: Another area in which the garbage collector will influence your designs and implementations is in object clean up. In C++ there is a destructor that does the opposite job of the constructor. The destructor's primary use is to explicitly free any memory pointed to by data members of the object. There is, of course, no need to do this in Java, because the garbage collector takes care of freeing memory. However, C++ destructors were also used to release any non-memory resources, such as file handles or graphic display contexts, that may have been used by the object. It is important to release resources such as these, because there is a finite number of them available to the program. If you do not release these kinds of resources when you are done with them, your program may run out of them. In Java, such resources are not released by the garbage collector, so the programmer must release them explicitly. Because Java objects have no destructors, the design challenge becomes one of making sure non-memory resources will be released when an object doesn't need them anymore.
A straightforward solution is to design objects that don't hang onto a resource for their entire lifetimes. Instead, aim for objects that grab a resource only when it is needed during a method invocation, and then release the resource before returning from the method. Objects designed in this way don't need cleanup. However, this won't work in all cases. So, when you have an object that requires a release of non- memory resources upon the object's "death," you should define a method that performs this service, and invoke it on your object when your program no longer needs the object. This method will end up being invoked where you would have explicitly deallocated an object in a non-garbage-collected language such as C++. So it resurrects that old problem of figuring out when an object is no longer needed. You'll probably want to write this function to recognize that it has been accidentally called twice and either throw an exception or ignore the second call. Also, you could even put a check in the finalizer of the class to make sure the cleanup routine has been called. If it hasn't been called, the finalizer could call it.
Finalizers: This brings us to finalizers. You may be wondering, why can't a finalizer be used for clean up? Isn't it kind of like a destructor? It turns out that often the best finalizer is no finalizer.
Finalizers are guaranteed to be called before the memory used by an object is reclaimed. However, there is no guarantee that any object's memory will ever be reclaimed. Hence, there is no guarantee that a finalizer will ever be called. (There is a promise that, barring catastrophic error conditions, all finalizers will be run on any left-over objects when the Java Virtual Machine exits; However, this likely too late if your program is waiting for a file handle to be released. (Besides, I'm not convinced it happens anyway.)) Therefore, it is vital that you never rely on a finalizer to free finite resources, such as file handles, that may be needed later by your program.
You might be tempted to use the finalizer as your clean up routine, given that it is perfectly legal in Java to invoke a finalizer explicitly just like any other method. You could write the finalizer such that it does the clean up the first time it is invoked, and does nothing any subsequent time it is invoked. In this way it would serve as a fail safe in case you ever forget to invoke it explicitly. However, this is not the best approach because finalizers require CPU time to run. (One finalizer that just checks an instance variable, discovers the method has already been finalized, and returns won't take too long. But if your program generates 1,000,000 of those objects, the small amount of time for each could add up to something significant.) Also, objects without finalizers are "easier" for the garbage collector to dispose of. The heap space for an object might get reclaimed sooner if that object doesn't have a finalizer. (Explain the process?) Therefore, it is usually better that clean up be done by a method that is not the finalizer, so that clean up is done in a program thread, not a garbage collector thread. In this manner you can retain control of precisely when the clean up happens, and ease the garbage collector's demands on CPU time.
[A] Memory Management
Stress: One last thing you need to keep in mind in your designs and implementations is the garbage collector's stress level. If your program is generating millions of little objects and discarding them shortly after their creation, you could be placing a heavy burden on the garbage collector, which could degrade the performance of your program. Because each Java Virtual Machine can implement its garbage collector differently, giving lots of work to the garbage collector can affect your program's performance differently on different platforms. One way to combat this is to redesign your program so it doesn't create millions of little objects with short lifetimes. If this is not possible, you may want to create a container class that allows you to reuse little objects that have come to the end of their short lifetimes. By reusing an object, rather than discarding it and allocating a new one, you reduce the garbage collector's work load. Garbage collection doesn't solve all memory management problems. You could still run out of memory, and you could still have a memory leak. Don't leave unused references to objects lying around. Null out the reference. It's OK to return something you allocated. A garbage-collected heap solves certain design dilemmas common in environments where programmers must do their own deallocation, such as returning objects created by a method. Returning objects: Having a garbage-collected heap allows you to treat objects in your Java programs a bit more like you treat them in the real world. In a language such as C or C++, where the programmer must explicitly deallocate memory, there is a pesky design issue whenever a function needs to return a pointer to some memory that it allocated. It is common among programmers to consider such a function unfriendly, because the function requires some other function to free the memory it allocated. Idealists pontificate about the virtuous program in which all functions, before returning, free any memory that they allocate. Indeed this is a worthy goal, but unfortunately not always practical. In Java, the issue goes away. If you find yourself in a similar situation in a Java program, just create the new object and return it. When the recipient of the object is done with it, the garbage collector will take care of freeing the memory. A good example is a method from which you want to return several ints. In Java, you can guiltlessly allocate an array of ints and return the array. When the program is done with the array object, the garbage collector will reclaim the memory. Can say: A Java object does not have a destructor, like C++ objects. Given the architecture of the JVM, Java object lives to not have clean, well-defined ends. Where a C++ object experiences a traumatic and sudden death, a Java object kind of fades away.
Why clean up?
Every object in a Java program uses computing resources that are
finite. Most obviously, all objects use some memory to store their
images on the heap. (This is true even for objects that declare no
instance variables. Each object image must include some kind of pointer
to class data, and can include other implementation-dependent
information as well.) But objects may also use other finite resources
besides memory. For example, some objects may use resources such as
file handles, graphics contexts, sockets, and so on. When you design an
object, you must make sure it eventually releases any finite
resources it uses so the system won't run out of those resources.
Because Java is a garbage-collected language, releasing the memory associated with an object is easy. All you need to do is let go of all references to the object. Because you don't have to worry about explicitly freeing an object, as you must in languages such as C or C++, you needn't worry about corrupting memory by accidentally freeing the same object twice. You do, however, need to make sure you actually release all references to the object. If you don't, you can end up with a memory leak, just like the memory leaks you get in a C++ program when you forget to explicitly free objects. Nevertheless, so long as you release all references to an object, you needn't worry about explicitly "freeing" that memory.
Similarly, you needn't worry about explicitly freeing any constituent objects referenced by the instance variables of an object you no longer need. Releasing all references to the unneeded object will in effect invalidate any constituent object references contained in that object's instance variables. If the now-invalidated references were the only remaining references to those constituent objects, the constituent objects will also be available for garbage collection. Piece of cake, right?
The rules of garbage collection
Although garbage collection does indeed make memory management in Java
a lot easier than it is in C or C++, you aren't able to completely
forget about memory when you program in Java. To know when you may need
to think about memory management in Java, you need to know a bit about
the way garbage collection is treated in the Java specifications.
Garbage collection is not mandated
The first thing to know is that no matter how diligently you search
through the Java Virtual Machine Specification (JVM Spec), you won't be able
to find any sentence that commands, Every JVM must have a garbage
collector. The Java Virtual Machine Specification gives VM designers a
great deal of leeway in deciding how their implementations will manage
memory, including deciding whether or not to even use garbage
collection at all. Thus, it is possible that some JVMs (such as a
bare-bones smart card JVM) may require that programs executed in each
session "fit" in the available memory.
Of course, you can always run out of memory, even on a virtual memory
system. The JVM Spec does not state how much memory will be available
to a JVM. It just states that whenever a JVM does run out of
memory, it should throw an OutOfMemoryError
.
Nevertheless, to give Java applications the best chance of executing without running out of memory, most JVMs will use a garbage collector. The garbage collector reclaims the memory occupied by unreferenced objects on the heap, so that memory can be used again by new objects, and usually de-fragments the heap as the program runs.
Garbage collection algorithm is not defined
Another command you won't find in the JVM specification is All JVMs
that use garbage collection must use the XXX algorithm. The designers
of each JVM get to decide how garbage collection will work in their
implementations. Garbage collection algorithm is one area in which JVM
vendors can strive to make their implementation better than the
competition's. This is significant for you as a Java programmer for the
following reason:
Because you don't generally know how garbage collection will be performed inside a JVM, you don't know when any particular object will be garbage collected.
So what? you might ask. The reason you might care when an object is
garbage collected has to do with finalizers. (A finalizer is
defined as a regular Java instance method named finalize()
that returns void and takes no arguments.) The Java specifications make
the following promise about finalizers:
Before reclaiming the memory occupied by an object that has a finalizer, the garbage collector will invoke that object's finalizer.
Given that you don't know when objects will be garbage collected, but you do know that finalizable objects will be finalized as they are garbage collected, you can make the following grand deduction:
You don't know when objects will be finalized.
You should imprint this important fact on your brain and forever allow it to inform your Java object designs.
Finalizers to avoid
The central rule of thumb concerning finalizers is this:
Don't design your Java programs such that correctness depends upon "timely" finalization.
In other words, don't write programs that will break if certain objects aren't finalized by certain points in the life of the program's execution. If you write such a program, it may work on some implementations of the JVM but fail on others.
Don't rely on finalizers to release non-memory resources
An example of an object that breaks this rule is one that opens a file
in its constructor and closes the file in its finalize()
method. Although this design seems neat, tidy, and symmetrical, it
potentially creates an insidious bug. A Java program generally will
have only a finite number of file handles at its disposal. When all
those handles are in use, the program won't be able to open any more
files.
A Java program that makes use of such an object (one that opens a file in its constructor and closes it in its finalizer) may work fine on some JVM implementations. On such implementations, finalization would occur often enough to keep a sufficient number of file handles available at all times. But the same program may fail on a different JVM whose garbage collector doesn't finalize often enough to keep the program from running out of file handles. Or, what's even more insidious, the program may work on all JVM implementations now but fail in a mission-critical situation a few years (and release cycles) down the road.
Other finalizer rules of thumb
Two other decisions left to JVM designers are selecting the thread (or
threads) that will execute the finalizers and the order in which
finalizers will be run. Finalizers may be run in any order --
sequentially by a single thread or concurrently by multiple threads. If
your program somehow depends for correctness on finalizers being run in
a particular order, or by a particular thread, it may work on some JVM
implementations but fail on others.
You should also keep in mind that Java considers an object to be
finalized whether the finalize()
method returns normally
or completes abruptly by throwing an exception. Garbage collectors
ignore any exceptions thrown by finalizers and in no way notify the
rest of the application that an exception was thrown. If you need to
ensure that a particular finalizer fully accomplishes a certain
mission, you must write that finalizer so that it handles any
exceptions that may arise before the finalizer completes its mission.
One more rule of thumb about finalizers concerns objects left on
the heap at the end of the application's lifetime. By default, the
garbage collector will not execute the finalizers of any objects left
on the heap when the application exits. To change this default, you
must invoke the runFinalizersOnExit()
method of class
Runtime
or System
, passing true
as the single parameter. If your program contains objects whose
finalizers must absolutely be invoked before the program exits, be sure
to invoke runFinalizersOnExit()
somewhere in your
program.
So what are finalizers good for?
By now you may be getting the feeling that you don't have much use for
finalizers. While it is likely that most of the classes you design
won't include a finalizer, there are some reasons to use finalizers.
One reasonable, though rare, application for a finalizer is to free
memory allocated by native methods. If an object invokes a native
method that allocates memory (perhaps a C function that calls
malloc()
), that object's finalizer could invoke a native
method that frees that memory (calls free()
). In this
situation, you would be using the finalizer to free up memory allocated
on behalf of an object -- memory that will not be automatically
reclaimed by the garbage collector.
Another, more common, use of finalizers is to provide a fallback
mechanism for releasing non-memory finite resources such as file
handles or sockets. As mentioned previously, you shouldn't rely on
finalizers for releasing finite non-memory resources. Instead, you
should provide a method that will release the resource. But you may
also wish to include a finalizer that checks to make sure the resource
has already been released, and if it hasn't, that goes ahead and
releases it. Such a finalizer guards against (and hopefully will not
encourage) sloppy use of your class. If a client programmer forgets to
invoke the method you provided to release the resource, the
finalizer will release the resource if the object is ever garbage
collected. The finalize()
method of the
LogFileManager
class, shown later in this article, is an
example of this kind of finalizer.
Avoid finalizer abuse
The existence of finalization produces some interesting complications
for JVMs and some interesting possibilities for Java programmers. For a
discussion of the impact of finalizers on JVMs, see the sidebar, a short excerpt from chapter 9,
"Garbage Collection," of my book, Inside the Java
Virtual Machine.
What finalization grants to programmers is power over the life and death of objects. In short, it is possible and completely legal in Java to resurrect objects in finalizers -- to bring them back to life by making them referenced again. (One way a finalizer could accomplish this is by adding a reference to the object being finalized to a static linked list that is still "live.") Although such power may be tempting to exercise because it makes you feel important, the rule of thumb is to resist the temptation to use this power. In general, resurrecting objects in finalizers constitutes finalizer abuse.
The main justification for this rule is that any program that uses resurrection can be redesigned into an easier-to-understand program that doesn't use resurrection. A formal proof of this theorem is left as an exercise to the reader (I've always wanted to say that), but in an informal spirit, consider that object resurrection will be as random and unpredictable as object finalization. As such, a design that uses resurrection will be difficult to figure out by the next maintenance programmer who happens along -- who may not fully understand the idiosyncrasies of garbage collection in Java.
If you feel you simply must bring an object back to life, consider
cloning a new copy of the object instead of resurrecting the same old
object. The reasoning behind this piece of advice is that garbage
collectors in the JVM invoke the finalize()
method of an
object only once. If that object is resurrected and becomes
available for garbage collection a second time, the object's
finalize()
method will not be invoked again.
Managing non-memory resources
Because heap memory is automatically reclaimed by the garbage
collector, the main thing you need to worry about when you design an
object's end-of-lifetime behavior is to ensure that finite non-memory
resources, such as file handles or sockets, are released. You can take
any of three basic approaches when you design an object that needs to
use a finite non-memory resource:
Approach 1: Obtain and release within each relevant method
As a general rule, the releasing of non-memory finite resources should
be done as soon as possible after their use because the resources are,
by definition, finite. If possible, you should try to obtain a
resource, use it, then release it all within the method that needs the
resource.
A log file class: An example of Approach 1
An example of a class where Approach 1 might make sense is a log file
class. Such a class takes care of formatting and writing log messages
to a file. The name of the log file is passed to the object as it is
instantiated. To write a message to the log file, a client invokes a
method in the log file class, passing the message as a
String
. Here's an example:
import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.IOException; class LogFile { private String fileName; LogFile(String fileName) { this.fileName = fileName; } // The writeToFile() method will catch any IOException // so that clients aren't forced to catch IOException // everywhere they write to the log file. For now, // just fail silently. In the future, could put // up an informative non-modal dialog box that indicates // a logging error occurred. - bv 4/15/98 void writeToFile(String message) { FileOutputStream fos = null; PrintWriter pw = null; try { fos = new FileOutputStream(fileName, true); try { pw = new PrintWriter(fos, false); pw.println("------------------"); pw.println(message); pw.println(); } finally { if (pw != null) { pw.close(); } } } catch (IOException e) { } finally { if (fos != null) { try { fos.close(); } catch (IOException e) { } } } } }
Class LogFile
is a simple example of Approach 1. A more
production-ready LogFile
class might do things such as:
The main feature of this simple version of class LogFile
is that it surrounds each log message with a series of dashes and a
blank line.
Using finally to ensure resource release
Note that in the writeToFile()
method, the releasing of
the resource is done in finally clauses. This is to make sure the
finite resource (file handle) is actually released no matter how the
code is exited. If an IOException
is thrown, the file will
be closed.
Pros and cons of Approach 1
The approach to resource management taken by class LogFile
(Approach 1 from the above list) helps make your class easy to use,
because client programmers don't have to worry about explicitly
obtaining or releasing the resource. In both Approach 2 and 3 from the list above
client programmers must remember to explicitly invoke a
method to release the resource. In addition -- and what can be far more
difficult -- client programmers must figure out when their programs no
longer need a resource.
A problem with Approach 1 is that obtaining and releasing the
resource each time you need it may be too inefficient. Another problem
is that, in some situations, you may need to hold onto the resource
between invocations of methods that use the resource (such as
writeToFile()
), so no other object can have access to it.
In such cases, one of the other two approaches is preferable.
Approach 2: Offer methods for obtaining and releasing resources
In Approach 2 from the list above, you provide one method for
obtaining the resource and another method for releasing it. This
approach enables the same class instance to obtain and release a
resource multiple times. Here's an example:
import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.IOException; class LogFileManager { private FileOutputStream fos; private PrintWriter pw; private boolean logFileOpen = false; LogFileManager() { } LogFileManager(String fileName) throws IOException { openLogFile(fileName); } void openLogFile(String fileName) throws IOException { if (!logFileOpen) { try { fos = new FileOutputStream(fileName, true); pw = new PrintWriter(fos, false); logFileOpen = true; } catch (IOException e) { if (pw != null) { pw.close(); pw = null; } if (fos != null) { fos.close(); fos = null; } throw e; } } } void closeLogFile() throws IOException { if (logFileOpen) { pw.close(); pw = null; fos.close(); fos = null; logFileOpen = false; } } boolean isOpen() { return logFileOpen; } void writeToFile(String message) throws IOException { pw.println("------------------"); pw.println(message); pw.println(); } protected void finalize() throws Throwable { if (logFileOpen) { try { closeLogFile(); } finally { super.finalize(); } } } }
In this example, class LogFileManager
declares methods
openLogFile()
and closeLogFile()
. Given this
design, you could write to multiple log files with one instance of this
class. This design also allows a client to monopolize the resource for
as long as it wants. A client can write several consecutive messages to
the log file without fear that another thread or process will slip in
any intervening messages. Once a client successfully opens a log file
with openLogFile()
, that log file belongs exclusively to
that client until the client invokes closeLogFile()
.
Note that LogFileManager
uses a finalizer as a fallback
in case a client forgets to invoke closeLogFile()
. As
mentioned earlier in this article, this is one of the more common uses
of finalizers.
Note also that after invoking closeLogFile()
,
LogFileManager
's finalizer invokes
super.finalize()
. Invoking superclass finalizers is good
practice in any finalizer, even in cases (such as this) where no
superclass exists other than Object
. The JVM does not
automatically invoke superclass finalizers, so you must do so
explicitly. If someone ever inserts a class that declares a finalizer
between LogFileManager
and Object
in the
inheritance hierarchy, the new object's finalizer will already be
invoked by LogFileManager
's existing finalizer.
Making super.finalize()
the last action of a finalizer
ensures that subclasses will be finalized before superclasses. Although
in most cases the placement of super.finalize()
won't
matter, in some rare cases, a subclass finalizer may require that its
superclass be as yet unfinalized. So, as a general rule of thumb, place
super.finalize()
last.
Approach 3: Claim resource on creation, offer method for release
In the last approach, Approach 3 from the above list, the object
obtains the resource upon creation and declares a method that releases
the resource. Here's an example:
import java.io.FileOutputStream; import java.io.PrintWriter; import java.io.IOException; class LogFileTransaction { private FileOutputStream fos; private PrintWriter pw; private boolean logFileOpen = false; LogFileTransaction(String fileName) throws IOException { try { fos = new FileOutputStream(fileName, true); pw = new PrintWriter(fos, false); logFileOpen = true; } catch (IOException e) { if (pw != null) { pw.close(); pw = null; } if (fos != null) { fos.close(); fos = null; } throw e; } } void closeLogFile() throws IOException { if (logFileOpen) { pw.close(); pw = null; fos.close(); fos = null; logFileOpen = false; } } boolean isOpen() { return logFileOpen; } void writeToFile(String message) throws IOException { pw.println("------------------"); pw.println(message); pw.println(); } protected void finalize() throws Throwable { if (logFileOpen) { try { closeLogFile(); } finally { super.finalize(); } } } }
This class is called LogFileTransaction
because every time
a client wants to write a chunk of messages to the log file (and then
let others use that log file), it must create a new
LogFileTransaction
. Thus, this class models one
transaction between the client and the log file.
One interesting thing to note about Approach 3 is that this is the
approach used by the FileOutputStream
and
PrintWriter
classes used by all three example log file
classes. In fact, if you look through the java.io
package,
you'll find that almost all of the java.io
classes that
deal with file handles use Approach 3. (The two exceptions are
PipedReader
and PipedWriter
, which use
Approach 2.)
The <init>
method is not actually part of the Java
language. Rather, it is something the Java virtual machine expects to
see in a Java class file. This distinction is significant because the
Java language does not depend on the class file. Java source can be
compiled into other binary formats, including native executables. A
Java compiler that translates Java language source into some other
binary format need not generate a method named
<init>
, so long as objects are initialized in the
proper way at the proper time. The Java Language Specification (JLS) details
the order of initialization and when it occurs, but doesn't say how it
is actually accomplished.
Still,
understanding how initialization works inside class files can help you
understand the order of initialization in the language.
The Java compiler resolves references to fields that are both static
and
final
, and initialized by a compile-time constant expression, to a copy of the
constant value. An example of two constant fields are shown below as part of class
CoffeeCup
. The maxCapacity
field is initialized by a compile-
time constant, but the currentCoffeeAmount
field is not. References to the
maxCapacity
field are compiled to refer directly to a constant pool entry with the
constant value 250. The maxCapacity
field is not initialized by the
<clinit>
method. However, because the constant value of
currentCoffeeAmount
is not known at compile time, it must be initialized by
<clinit>
. The Java Virtual Machine will execute the
<clinit>
method during the initialization phase of the class, and the
<clinit>
method will invoke Math.random
, multiply
the result by 250, and assign currentCoffeeAmount
its initial value, which will
thereafter remain constant.
class CoffeeCup { static final int maxCapacity = 250; // milliliters static final int currentCoffeeAmount = (int) (Math.random() * 250d); }
The use or assignment of a static
, final
field is not an
active use of a class, because such fields are not initialized via the <clinit>
method. A constant field is a class variable that is declared final
as well as
static
, and is initialized with a compile-time constant expression. Such fields are
resolved to constant expressions To initialize a class, the Java Virtual Machine executes the class's
<clinit>
method, if it has one. A class's superclass must be initialized
before the class is initialized, so the Java Virtual Machine will invoke the
<clinit>
methods of the class's superclass if it hasn't yet been invoked.
The code of a <clinit>
method does not explicitly invoke a superclass's
<clinit>
method, therefore the Java Virtual Machine must make certain
the <clinit>
methods of superclasses have been executed before it invokes
the <clinit>
method of a class.
To initialize an interface, the Java Virtual Machine executes the interface's
<clinit>
method, if it has one, just as it does for classes. An interface may
have a <clinit>
method because interfaces can contain constants. In
interfaces, constants are public
, final
,
static
variables. The variables must have an initialization expression, which is the
interface version of a class variable initializer. Interfaces cannot have static initialization blocks. As with
classes, only initializers that require evaluation of expressions that are not compile-time constants will end
up as code in the <clinit>
method. Therefore, if an interface has at least
one constant that is initialized by an expression that is not a compile time constant, that interface will
have a <clinit>
method. Initialization of an interface does not require
prior initialization of its superinterfaces.
[SHOW THAT THESE ARE PUT INTO SEPARATE CLASS FILES AND THAT THEY ARE USED BY THE CASE STATEMENTS OF SWITCHES, AND TO DO CONDITIONAL COMPILATION.]
Sponsored Links
|