4. Initialization & Cleanup▲
As the computer revolution progresses, "unsafe" programming has become one of the major culprits that makes programming expensive.
Two of these safety issues are initialization and cleanup. Many C bugs occur when the programmer forgets to initialize a variable. This is especially true with libraries when users don't know how to initialize a library component, or even that they must. Cleanup is a special problem because it's easy to forget about an element when you're done with it, since it no longer concerns you. Thus, the resources used by that element are retained and you can easily end up running out of resources (most notably, memory).
C++ introduced the concept of a constructor, a special method automatically called when an object is created. Java also adopted the constructor, and in addition has a garbage collector that automatically releases memory resources when they're no longer being used. This chapter examines the issues of initialization and cleanup, and their support in Java.
4-1. Guaranteed initialization with the constructor▲
You can imagine creating a method called initialize( ) for every class you write. The name is a hint that it should be called before using the object. Unfortunately, this means the user must remember to call the method. In Java, the class designer can guarantee initialization of every object by providing a special method called a constructor. If a class has a constructor, Java automatically calls that constructor when an object is created, before users can even get their hands on it. So initialization is guaranteed.
The next challenge is what to name this method. There are two issues. The first is that any name you use could clash with a name you might like to use as a member in the class. The second is that because the compiler is responsible for calling the constructor, it must always know which method to call. The C++ solution seems the easiest and most logical, so it's also used in Java: The name of the constructor is the same as the name of the class. It makes sense that such a method will be called automatically on initialization.
Here's a simple class with a constructor:
//: c04:SimpleConstructor.java
// Demonstration of a simple constructor.
import
com.bruceeckel.simpletest.*;
class
Rock {
Rock
(
) {
// This is the constructor
System.out.println
(
"Creating Rock"
);
}
}
public
class
SimpleConstructor {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
for
(
int
i =
0
; i <
10
; i++
)
new
Rock
(
);
monitor.expect
(
new
String[] {
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
,
"Creating Rock"
}
);
}
}
///:~
Now, when an object is created:
new
Rock
(
);
storage is allocated and the constructor is called. It is guaranteed that the object will be properly initialized before you can get your hands on it.
Note that the coding style of making the first letter of all methods lowercase does not apply to constructors, since the name of the constructor must match the name of the class exactly.
Like any method, the constructor can have arguments to allow you to specify how an object is created. The preceding example can easily be changed so the constructor takes an argument:
//: c04:SimpleConstructor2.java
// Constructors can have arguments.
import
com.bruceeckel.simpletest.*;
class
Rock2 {
Rock2
(
int
i) {
System.out.println
(
"Creating Rock number "
+
i);
}
}
public
class
SimpleConstructor2 {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
for
(
int
i =
0
; i <
10
; i++
)
new
Rock2
(
i);
monitor.expect
(
new
String[] {
"Creating Rock number 0"
,
"Creating Rock number 1"
,
"Creating Rock number 2"
,
"Creating Rock number 3"
,
"Creating Rock number 4"
,
"Creating Rock number 5"
,
"Creating Rock number 6"
,
"Creating Rock number 7"
,
"Creating Rock number 8"
,
"Creating Rock number 9"
}
);
}
}
///:~
Constructor arguments provide you with a way to provide parameters for the initialization of an object. For example, if the class Tree has a constructor that takes a single integer argument denoting the height of the tree, you would create a Tree object like this:
Tree t =
new
Tree
(
12
); // 12-foot tree
If Tree(int) is your only constructor, then the compiler won't let you create a Tree object any other way.
Constructors eliminate a large class of problems and make the code easier to read. In the preceding code fragment, for example, you don't see an explicit call to some initialize( ) method that is conceptually separate from creation. In Java, creation and initialization are unified concepts-you can't have one without the other.
The constructor is an unusual type of method because it has no return value. This is distinctly different from a void return value, in which the method returns nothing but you still have the option to make it return something else. Constructors return nothing and you don't have an option (the new expression does return a reference to the newly-created object, but the constructor itself has no return value). If there were a return value, and if you could select your own, the compiler would somehow need to know what to do with that return value.
4-2. Method overloading▲
One of the important features in any programming language is the use of names. When you create an object, you give a name to a region of storage. A method is a name for an action. By using names to describe your system, you create a program that is easier for people to understand and change. It's a lot like writing prose-the goal is to communicate with your readers.
You refer to all objects and methods by using names. Well-chosen names make it easier for you and others to understand your code.
A problem arises when mapping the concept of nuance in human language onto a programming language. Often, the same word expresses a number of different meanings-it's overloaded. This is useful, especially when it comes to trivial differences. You say "wash the shirt," "wash the car," and "wash the dog." It would be silly to be forced to say, "shirtWash the shirt," "carWash the car," and "dogWash the dog" just so the listener doesn't need to make any distinction about the action performed. Most human languages are redundant, so even if you miss a few words, you can still determine the meaning. We don't need unique identifiers-we can deduce meaning from context.
Most programming languages (C in particular) require you to have a unique identifier for each function. So you could not have one function called print( ) for printing integers and another called print( ) for printing floats-each function requires a unique name.
In Java (and C++), another factor forces the overloading of method names: the constructor. Because the constructor's name is predetermined by the name of the class, there can be only one constructor name. But what if you want to create an object in more than one way? For example, suppose you build a class that can initialize itself in a standard way or by reading information from a file. You need two constructors, one that takes no arguments (the default constructor, (19) also called the no-arg constructor), and one that takes a String as an argument, which is the name of the file from which to initialize the object. Both are constructors, so they must have the same name-the name of the class. Thus, method overloading is essential to allow the same method name to be used with different argument types. And although method overloading is a must for constructors, it's a general convenience and can be used with any method.
Here's an example that shows both overloaded constructors and overloaded ordinary methods:
//: c04:Overloading.java
// Demonstration of both constructor
// and ordinary method overloading.
import
com.bruceeckel.simpletest.*;
import
java.util.*;
class
Tree {
int
height;
Tree
(
) {
System.out.println
(
"Planting a seedling"
);
height =
0
;
}
Tree
(
int
i) {
System.out.println
(
"Creating new Tree that is "
+
i +
" feet tall"
);
height =
i;
}
void
info
(
) {
System.out.println
(
"Tree is "
+
height +
" feet tall"
);
}
void
info
(
String s) {
System.out.println
(
s +
": Tree is "
+
height +
" feet tall"
);
}
}
public
class
Overloading {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
for
(
int
i =
0
; i <
5
; i++
) {
Tree t =
new
Tree
(
i);
t.info
(
);
t.info
(
"overloaded method"
);
}
// Overloaded constructor:
new
Tree
(
);
monitor.expect
(
new
String[] {
"Creating new Tree that is 0 feet tall"
,
"Tree is 0 feet tall"
,
"overloaded method: Tree is 0 feet tall"
,
"Creating new Tree that is 1 feet tall"
,
"Tree is 1 feet tall"
,
"overloaded method: Tree is 1 feet tall"
,
"Creating new Tree that is 2 feet tall"
,
"Tree is 2 feet tall"
,
"overloaded method: Tree is 2 feet tall"
,
"Creating new Tree that is 3 feet tall"
,
"Tree is 3 feet tall"
,
"overloaded method: Tree is 3 feet tall"
,
"Creating new Tree that is 4 feet tall"
,
"Tree is 4 feet tall"
,
"overloaded method: Tree is 4 feet tall"
,
"Planting a seedling"
}
);
}
}
///:~
A Tree object can be created either as a seedling, with no argument, or as a plant grown in a nursery, with an existing height. To support this, there is a default constructor, and one that takes the existing height.
You might also want to call the info( ) method in more than one way. For example, if you have an extra message you want printed, you can use info(String), and info( ) if you have nothing more to say. It would seem strange to give two separate names to what is obviously the same concept. Fortunately, method overloading allows you to use the same name for both.
4-2-1. Distinguishing overloaded methods▲
If the methods have the same name, how can Java know which method you mean? There's a simple rule: each overloaded method must take a unique list of argument types.
If you think about this for a second, it makes sense. How else could a programmer tell the difference between two methods that have the same name, other than by the types of their arguments?
Even differences in the ordering of arguments are sufficient to distinguish two methods: (Although you don't normally want to take this approach, as it produces difficult-to-maintain code.)
//: c04:OverloadingOrder.java
// Overloading based on the order of the arguments.
import
com.bruceeckel.simpletest.*;
public
class
OverloadingOrder {
static
Test monitor =
new
Test
(
);
static
void
print
(
String s, int
i) {
System.out.println
(
"String: "
+
s +
", int: "
+
i);
}
static
void
print
(
int
i, String s) {
System.out.println
(
"int: "
+
i +
", String: "
+
s);
}
public
static
void
main
(
String[] args) {
print
(
"String first"
, 11
);
print
(
99
, "Int first"
);
monitor.expect
(
new
String[] {
"String: String first, int: 11"
,
"int: 99, String: Int first"
}
);
}
}
///:~
The two print( ) methods have identical arguments, but the order is different, and that's what makes them distinct.
4-2-2. Overloading with primitives▲
A primitive can be automatically promoted from a smaller type to a larger one, and this can be slightly confusing in combination with overloading. The following example demonstrates what happens when a primitive is handed to an overloaded method:
//: c04:PrimitiveOverloading.java
// Promotion of primitives and overloading.
import
com.bruceeckel.simpletest.*;
public
class
PrimitiveOverloading {
static
Test monitor =
new
Test
(
);
void
f1
(
char
x) {
System.out.println
(
"f1(char)"
); }
void
f1
(
byte
x) {
System.out.println
(
"f1(byte)"
); }
void
f1
(
short
x) {
System.out.println
(
"f1(short)"
); }
void
f1
(
int
x) {
System.out.println
(
"f1(int)"
); }
void
f1
(
long
x) {
System.out.println
(
"f1(long)"
); }
void
f1
(
float
x) {
System.out.println
(
"f1(float)"
); }
void
f1
(
double
x) {
System.out.println
(
"f1(double)"
); }
void
f2
(
byte
x) {
System.out.println
(
"f2(byte)"
); }
void
f2
(
short
x) {
System.out.println
(
"f2(short)"
); }
void
f2
(
int
x) {
System.out.println
(
"f2(int)"
); }
void
f2
(
long
x) {
System.out.println
(
"f2(long)"
); }
void
f2
(
float
x) {
System.out.println
(
"f2(float)"
); }
void
f2
(
double
x) {
System.out.println
(
"f2(double)"
); }
void
f3
(
short
x) {
System.out.println
(
"f3(short)"
); }
void
f3
(
int
x) {
System.out.println
(
"f3(int)"
); }
void
f3
(
long
x) {
System.out.println
(
"f3(long)"
); }
void
f3
(
float
x) {
System.out.println
(
"f3(float)"
); }
void
f3
(
double
x) {
System.out.println
(
"f3(double)"
); }
void
f4
(
int
x) {
System.out.println
(
"f4(int)"
); }
void
f4
(
long
x) {
System.out.println
(
"f4(long)"
); }
void
f4
(
float
x) {
System.out.println
(
"f4(float)"
); }
void
f4
(
double
x) {
System.out.println
(
"f4(double)"
); }
void
f5
(
long
x) {
System.out.println
(
"f5(long)"
); }
void
f5
(
float
x) {
System.out.println
(
"f5(float)"
); }
void
f5
(
double
x) {
System.out.println
(
"f5(double)"
); }
void
f6
(
float
x) {
System.out.println
(
"f6(float)"
); }
void
f6
(
double
x) {
System.out.println
(
"f6(double)"
); }
void
f7
(
double
x) {
System.out.println
(
"f7(double)"
); }
void
testConstVal
(
) {
System.out.println
(
"Testing with 5"
);
f1
(
5
);f2
(
5
);f3
(
5
);f4
(
5
);f5
(
5
);f6
(
5
);f7
(
5
);
}
void
testChar
(
) {
char
x =
'x'
;
System.out.println
(
"char argument:"
);
f1
(
x);f2
(
x);f3
(
x);f4
(
x);f5
(
x);f6
(
x);f7
(
x);
}
void
testByte
(
) {
byte
x =
0
;
System.out.println
(
"byte argument:"
);
f1
(
x);f2
(
x);f3
(
x);f4
(
x);f5
(
x);f6
(
x);f7
(
x);
}
void
testShort
(
) {
short
x =
0
;
System.out.println
(
"short argument:"
);
f1
(
x);f2
(
x);f3
(
x);f4
(
x);f5
(
x);f6
(
x);f7
(
x);
}
void
testInt
(
) {
int
x =
0
;
System.out.println
(
"int argument:"
);
f1
(
x);f2
(
x);f3
(
x);f4
(
x);f5
(
x);f6
(
x);f7
(
x);
}
void
testLong
(
) {
long
x =
0
;
System.out.println
(
"long argument:"
);
f1
(
x);f2
(
x);f3
(
x);f4
(
x);f5
(
x);f6
(
x);f7
(
x);
}
void
testFloat
(
) {
float
x =
0
;
System.out.println
(
"float argument:"
);
f1
(
x);f2
(
x);f3
(
x);f4
(
x);f5
(
x);f6
(
x);f7
(
x);
}
void
testDouble
(
) {
double
x =
0
;
System.out.println
(
"double argument:"
);
f1
(
x);f2
(
x);f3
(
x);f4
(
x);f5
(
x);f6
(
x);f7
(
x);
}
public
static
void
main
(
String[] args) {
PrimitiveOverloading p =
new
PrimitiveOverloading
(
);
p.testConstVal
(
);
p.testChar
(
);
p.testByte
(
);
p.testShort
(
);
p.testInt
(
);
p.testLong
(
);
p.testFloat
(
);
p.testDouble
(
);
monitor.expect
(
new
String[] {
"Testing with 5"
,
"f1(int)"
,
"f2(int)"
,
"f3(int)"
,
"f4(int)"
,
"f5(long)"
,
"f6(float)"
,
"f7(double)"
,
"char argument:"
,
"f1(char)"
,
"f2(int)"
,
"f3(int)"
,
"f4(int)"
,
"f5(long)"
,
"f6(float)"
,
"f7(double)"
,
"byte argument:"
,
"f1(byte)"
,
"f2(byte)"
,
"f3(short)"
,
"f4(int)"
,
"f5(long)"
,
"f6(float)"
,
"f7(double)"
,
"short argument:"
,
"f1(short)"
,
"f2(short)"
,
"f3(short)"
,
"f4(int)"
,
"f5(long)"
,
"f6(float)"
,
"f7(double)"
,
"int argument:"
,
"f1(int)"
,
"f2(int)"
,
"f3(int)"
,
"f4(int)"
,
"f5(long)"
,
"f6(float)"
,
"f7(double)"
,
"long argument:"
,
"f1(long)"
,
"f2(long)"
,
"f3(long)"
,
"f4(long)"
,
"f5(long)"
,
"f6(float)"
,
"f7(double)"
,
"float argument:"
,
"f1(float)"
,
"f2(float)"
,
"f3(float)"
,
"f4(float)"
,
"f5(float)"
,
"f6(float)"
,
"f7(double)"
,
"double argument:"
,
"f1(double)"
,
"f2(double)"
,
"f3(double)"
,
"f4(double)"
,
"f5(double)"
,
"f6(double)"
,
"f7(double)"
}
);
}
}
///:~
You'll see that the constant value 5 is treated as an int, so if an overloaded method is available that takes an int, it is used. In all other cases, if you have a data type that is smaller than the argument in the method, that data type is promoted. char produces a slightly different effect, since if it doesn't find an exact char match, it is promoted to int.
What happens if your argument is bigger than the argument expected by the overloaded method? A modification of the preceding program gives the answer:
//: c04:Demotion.java
// Demotion of primitives and overloading.
import
com.bruceeckel.simpletest.*;
public
class
Demotion {
static
Test monitor =
new
Test
(
);
void
f1
(
char
x) {
System.out.println
(
"f1(char)"
); }
void
f1
(
byte
x) {
System.out.println
(
"f1(byte)"
); }
void
f1
(
short
x) {
System.out.println
(
"f1(short)"
); }
void
f1
(
int
x) {
System.out.println
(
"f1(int)"
); }
void
f1
(
long
x) {
System.out.println
(
"f1(long)"
); }
void
f1
(
float
x) {
System.out.println
(
"f1(float)"
); }
void
f1
(
double
x) {
System.out.println
(
"f1(double)"
); }
void
f2
(
char
x) {
System.out.println
(
"f2(char)"
); }
void
f2
(
byte
x) {
System.out.println
(
"f2(byte)"
); }
void
f2
(
short
x) {
System.out.println
(
"f2(short)"
); }
void
f2
(
int
x) {
System.out.println
(
"f2(int)"
); }
void
f2
(
long
x) {
System.out.println
(
"f2(long)"
); }
void
f2
(
float
x) {
System.out.println
(
"f2(float)"
); }
void
f3
(
char
x) {
System.out.println
(
"f3(char)"
); }
void
f3
(
byte
x) {
System.out.println
(
"f3(byte)"
); }
void
f3
(
short
x) {
System.out.println
(
"f3(short)"
); }
void
f3
(
int
x) {
System.out.println
(
"f3(int)"
); }
void
f3
(
long
x) {
System.out.println
(
"f3(long)"
); }
void
f4
(
char
x) {
System.out.println
(
"f4(char)"
); }
void
f4
(
byte
x) {
System.out.println
(
"f4(byte)"
); }
void
f4
(
short
x) {
System.out.println
(
"f4(short)"
); }
void
f4
(
int
x) {
System.out.println
(
"f4(int)"
); }
void
f5
(
char
x) {
System.out.println
(
"f5(char)"
); }
void
f5
(
byte
x) {
System.out.println
(
"f5(byte)"
); }
void
f5
(
short
x) {
System.out.println
(
"f5(short)"
); }
void
f6
(
char
x) {
System.out.println
(
"f6(char)"
); }
void
f6
(
byte
x) {
System.out.println
(
"f6(byte)"
); }
void
f7
(
char
x) {
System.out.println
(
"f7(char)"
); }
void
testDouble
(
) {
double
x =
0
;
System.out.println
(
"double argument:"
);
f1
(
x);f2
((
float
)x);f3
((
long
)x);f4
((
int
)x);
f5
((
short
)x);f6
((
byte
)x);f7
((
char
)x);
}
public
static
void
main
(
String[] args) {
Demotion p =
new
Demotion
(
);
p.testDouble
(
);
monitor.expect
(
new
String[] {
"double argument:"
,
"f1(double)"
,
"f2(float)"
,
"f3(long)"
,
"f4(int)"
,
"f5(short)"
,
"f6(byte)"
,
"f7(char)"
}
);
}
}
///:~
Here, the methods take narrower primitive values. If your argument is wider, then you must cast to the necessary type by placing the type name inside parentheses. If you don't do this, the compiler will issue an error message.
You should be aware that this is a narrowing conversion, which means you might lose information during the cast. This is why the compiler forces you to do it-to flag the narrowing conversion.
4-2-3. Overloading on return values▲
It is common to wonder "Why only class names and method argument lists? Why not distinguish between methods based on their return values?" For example, these two methods, which have the same name and arguments, are easily distinguished from each other:
void
f
(
) {}
int
f
(
) {}
This works fine when the compiler can unequivocally determine the meaning from the context, as in int x = f( ). However, you can also call a method and ignore the return value. This is often referred to as calling a method for its side effect, since you don't care about the return value, but instead want the other effects of the method call. So if you call the method this way:
f
(
);
how can Java determine which f( ) should be called? And how could someone reading the code see it? Because of this sort of problem, you cannot use return value types to distinguish overloaded methods.
4-2-4. Default constructors▲
As mentioned previously, a default constructor (a.k.a. a "no-arg" constructor) is one without arguments that is used to create a "basic object." If you create a class that has no constructors, the compiler will automatically create a default constructor for you. For example:
//: c04:DefaultConstructor.java
class
Bird {
int
i;
}
public
class
DefaultConstructor {
public
static
void
main
(
String[] args) {
Bird nc =
new
Bird
(
); // Default!
}
}
///:~
The line
new
Bird
(
);
creates a new object and calls the default constructor, even though one was not explicitly defined. Without it, we would have no method to call to build our object. However, if you define any constructors (with or without arguments), the compiler will not synthesize one for you:
class
Hat {
Hat
(
int
i) {}
Hat
(
double
d) {}
}
Now if you say:
new
Hat
(
);
the compiler will complain that it cannot find a constructor that matches. It's as if when you don't put in any constructors, the compiler says "You are bound to need some constructor, so let me make one for you." But if you write a constructor, the compiler says "You've written a constructor so you know what you're doing; if you didn't put in a default it's because you meant to leave it out."
4-2-5. The this keyword▲
If you have two objects of the same type called a and b, you might wonder how it is that you can call a method f( ) for both those objects:
class
Banana {
void
f
(
int
i) {
/* ... */
}
}
Banana a =
new
Banana
(
), b =
new
Banana
(
);
a.f
(
1
);
b.f
(
2
);
If there's only one method called f( ), how can that method know whether it's being called for the object a or b?
To allow you to write the code in a convenient object-oriented syntax in which you "send a message to an object," the compiler does some undercover work for you. There's a secret first argument passed to the method f( ), and that argument is the reference to the object that's being manipulated. So the two method calls become something like:
Banana.f
(
a,1
);
Banana.f
(
b,2
);
This is internal and you can't write these expressions and get the compiler to accept them, but it gives you an idea of what's happening.
Suppose you're inside a method and you'd like to get the reference to the current object. Since that reference is passed secretly by the compiler, there's no identifier for it. However, for this purpose there's a keyword: this. The this keyword-which can be used only inside a method-produces the reference to the object the method has been called for. You can treat this reference just like any other object reference. Keep in mind that if you're calling a method of your class from within another method of your class, you don't need to use this. You simply call the method. The current this reference is automatically used for the other method. Thus you can say:
class
Apricot {
void
pick
(
) {
/* ... */
}
void
pit
(
) {
pick
(
); /* ... */
}
}
Inside pit( ), you could say this.pick( ) but there's no need to. (20) The compiler does it for you automatically. The this keyword is used only for those special cases in which you need to explicitly use the reference to the current object. For example, it's often used in return statements when you want to return the reference to the current object:
//: c04:Leaf.java
// Simple use of the "this" keyword.
import
com.bruceeckel.simpletest.*;
public
class
Leaf {
static
Test monitor =
new
Test
(
);
int
i =
0
;
Leaf increment
(
) {
i++
;
return
this
;
}
void
print
(
) {
System.out.println
(
"i = "
+
i);
}
public
static
void
main
(
String[] args) {
Leaf x =
new
Leaf
(
);
x.increment
(
).increment
(
).increment
(
).print
(
);
monitor.expect
(
new
String[] {
"i = 3"
}
);
}
}
///:~
Because increment( ) returns the reference to the current object via the this keyword, multiple operations can easily be performed on the same object.
4-2-5-1. Calling constructors from constructors▲
When you write several constructors for a class, there are times when you'd like to call one constructor from another to avoid duplicating code. You can make such a call by by using the this keyword.
Normally, when you say this, it is in the sense of "this object" or "the current object," and by itself it produces the reference to the current object. In a constructor, the this keyword takes on a different meaning when you give it an argument list. It makes an explicit call to the constructor that matches that argument list. Thus you have a straightforward way to call other constructors:
//: c04:Flower.java
// Calling constructors with "this."
import
com.bruceeckel.simpletest.*;
public
class
Flower {
static
Test monitor =
new
Test
(
);
int
petalCount =
0
;
String s =
new
String
(
"null"
);
Flower
(
int
petals) {
petalCount =
petals;
System.out.println
(
"Constructor w/ int arg only, petalCount= "
+
petalCount);
}
Flower
(
String ss) {
System.out.println
(
"Constructor w/ String arg only, s="
+
ss);
s =
ss;
}
Flower
(
String s, int
petals) {
this
(
petals);
//! this(s); // Can't call two!
this
.s =
s; // Another use of "this"
System.out.println
(
"String & int args"
);
}
Flower
(
) {
this
(
"hi"
, 47
);
System.out.println
(
"default constructor (no args)"
);
}
void
print
(
) {
//! this(11); // Not inside non-constructor!
System.out.println
(
"petalCount = "
+
petalCount +
" s = "
+
s);
}
public
static
void
main
(
String[] args) {
Flower x =
new
Flower
(
);
x.print
(
);
monitor.expect
(
new
String[] {
"Constructor w/ int arg only, petalCount= 47"
,
"String & int args"
,
"default constructor (no args)"
,
"petalCount = 47 s = hi"
}
);
}
}
///:~
The constructor Flower(String s, int petals) shows that, while you can call one constructor using this, you cannot call two. In addition, the constructor call must be the first thing you do, or you'll get a compiler error message.
This example also shows another way you'll see this used. Since the name of the argument s and the name of the member data s are the same, there's an ambiguity. You can resolve it using this.s, to say that you're referring to the member data. You'll often see this form used in Java code, and it's used in numerous places in this book.
In print( ) you can see that the compiler won't let you call a constructor from inside any method other than a constructor.
4-2-5-2. The meaning of static▲
With the this keyword in mind, you can more fully understand what it means to make a method static. It means that there is no this for that particular method. You cannot call non-static methods from inside static methods (21) (although the reverse is possible), and you can call a static method for the class itself, without any object. In fact, that's primarily what a static method is for. It's as if you're creating the equivalent of a global function (from C). However, global functions are not permitted in Java, and putting the static method inside a class allows it access to other static methods and to static fields.
Some people argue that static methods are not object-oriented, since they do have the semantics of a global function; with a static method, you don't send a message to an object, since there's no this. This is probably a fair argument, and if you find yourself using a lot of static methods, you should probably rethink your strategy. However, statics are pragmatic, and there are times when you genuinely need them, so whether or not they are "proper OOP" should be left to the theoreticians. Indeed, even Smalltalk has the equivalent in its "class methods."
4-3. Cleanup: finalization and garbage collection▲
Programmers know about the importance of initialization, but often forget the importance of cleanup. After all, who needs to clean up an int? But with libraries, simply "letting go" of an object once you're done with it is not always safe. Of course, Java has the garbage collector to reclaim the memory of objects that are no longer used. Now consider an unusual case: suppose your object allocates "special" memory without using new. The garbage collector only knows how to release memory allocated with new, so it won't know how to release the object's "special" memory. To handle this case, Java provides a method called finalize( ) that you can define for your class. Here's how it's supposed to work. When the garbage collector is ready to release the storage used for your object, it will first call finalize( ), and only on the next garbage-collection pass will it reclaim the object's memory. So if you choose to use finalize( ), it gives you the ability to perform some important cleanup at the time of garbage collection.
This is a potential programming pitfall because some programmers, especially C++ programmers, might initially mistake finalize( ) for the destructor in C++, which is a function that is always called when an object is destroyed. But it is important to distinguish between C++ and Java here, because in C++, objects always get destroyed (in a bug-free program), whereas in Java, objects do not always get garbage collected. Or, put another way:
- 1. Your objects might not get garbage collected.
- 2. Garbage collection is not destruction
If you remember this, you will stay out of trouble. What it means is that if there is some activity that must be performed before you no longer need an object, you must perform that activity yourself. Java has no destructor or similar concept, so you must create an ordinary method to perform this cleanup. For example, suppose that in the process of creating your object, it draws itself on the screen. If you don't explicitly erase its image from the screen, it might never get cleaned up. If you put some kind of erasing functionality inside finalize( ), then if an object is garbage collected and finalize( ) is called (there's no guarantee this will happen), then the image will first be removed from the screen, but if it isn't, the image will remain.
You might find that the storage for an object never gets released because your program never nears the point of running out of storage. If your program completes and the garbage collector never gets around to releasing the storage for any of your objects, that storage will be returned to the operating system en masse as the program exits. This is a good thing, because garbage collection has some overhead, and if you never do, it you never incur that expense.
4-3-1. What is finalize( ) for?▲
So, if you should not use finalize( ) as a general-purpose cleanup method, what good is it?
A third point to remember is:
- 3. Garbage collection is only about memory.
That is, the sole reason for the existence of the garbage collector is to recover memory that your program is no longer using. So any activity that is associated with garbage collection, most notably your finalize( ) method, must also be only about memory and its deallocation.
Does this mean that if your object contains other objects, finalize( ) should explicitly release those objects? Well, no-the garbage collector takes care of the release of all object memory regardless of how the object is created. It turns out that the need for finalize( ) is limited to special cases in which your object can allocate some storage in some way other than creating an object. But, you might observe, everything in Java is an object, so how can this be?
It would seem that finalize( ) is in place because of the possibility that you'll do something C-like by allocating memory using a mechanism other than the normal one in Java. This can happen primarily through native methods, which are a way to call non-Java code from Java. (Native methods are covered in Appendix B in the electronic 2nd edition of this book, available on this book's CD ROM and at www.BruceEckel.com.) C and C++ are the only languages currently supported by native methods, but since they can call subprograms in other languages, you can effectively call anything. Inside the non-Java code, C's malloc( ) family of functions might be called to allocate storage, and unless you call free( ), that storage will not be released, causing a memory leak. Of course, free( ) is a C and C++ function, so you'd need to call it in a native method inside your finalize( ).
After reading this, you probably get the idea that you won't use finalize( ) much. (22) You're correct; it is not the appropriate place for normal cleanup to occur. So where should normal cleanup be performed?
4-3-2. You must perform cleanup▲
To clean up an object, the user of that object must call a cleanup method at the point the cleanup is desired. This sounds pretty straightforward, but it collides a bit with the C++ concept of the destructor. In C++, all objects are destroyed. Or rather, all objects should be destroyed. If the C++ object is created as a local (i.e., on the stack-not possible in Java), then the destruction happens at the closing curly brace of the scope in which the object was created. If the object was created using new (like in Java), the destructor is called when the programmer calls the C++ operator delete (which doesn't exist in Java). If the C++ programmer forgets to call delete, the destructor is never called, and you have a memory leak, plus the other parts of the object never get cleaned up. This kind of bug can be very difficult to track down, and is one of the compelling reasons to move from C++ to Java.
In contrast, Java doesn't allow you to create local objects-you must always use new. But in Java, there's no "delete" to call to release the object, because the garbage collector releases the storage for you. So from a simplistic standpoint, you could say that because of garbage collection, Java has no destructor. You'll see as this book progresses, however, that the presence of a garbage collector does not remove the need for or utility of destructors. (And you should never call finalize( ) directly, so that's not an appropriate avenue for a solution.) If you want some kind of cleanup performed other than storage release, you must still explicitly call an appropriate method in Java, which is the equivalent of a C++ destructor without the convenience.
Remember that neither garbage collection nor finalization is guaranteed. If the JVM isn't close to running out of memory, then it might not waste time recovering memory through garbage collection.
4-3-3. The termination condition▲
In general, you can't rely on finalize( ) being called, and you must create separate "cleanup" methods and call them explicitly. So it appears that finalize( ) is only useful for obscure memory cleanup that most programmers will never use. However, there is a very interesting use of finalize( ) that does not rely on it being called every time. This is the verification of the termination condition(23) of an object.
At the point that you're no longer interested in an object-when it's ready to be cleaned up-that object should be in a state whereby its memory can be safely released. For example, if the object represents an open file, that file should be closed by the programmer before the object is garbage collected. If any portions of the object are not properly cleaned up, then you have a bug in your program that could be very difficult to find. The value of finalize( ) is that it can be used to eventually discover this condition, even if it isn't always called. If one of the finalizations happens to reveal the bug, then you discover the problem, which is all you really care about.
Here's a simple example of how you might use it:
//: c04:TerminationCondition.java
// Using finalize() to detect an object that
// hasn't been properly cleaned up.
import
com.bruceeckel.simpletest.*;
class
Book {
boolean
checkedOut =
false
;
Book
(
boolean
checkOut) {
checkedOut =
checkOut;
}
void
checkIn
(
) {
checkedOut =
false
;
}
public
void
finalize
(
) {
if
(
checkedOut)
System.out.println
(
"Error: checked out"
);
}
}
public
class
TerminationCondition {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
Book novel =
new
Book
(
true
);
// Proper cleanup:
novel.checkIn
(
);
// Drop the reference, forget to clean up:
new
Book
(
true
);
// Force garbage collection & finalization:
System.gc
(
);
monitor.expect
(
new
String[] {
"Error: checked out"
}
, Test.WAIT);
}
}
///:~
The termination condition is that all Book objects are supposed to be checked in before they are garbage collected, but in main( ), a programmer error doesn't check in one of the books. Without finalize( ) to verify the termination condition, this could be a difficult bug to find.
Note that System.gc( ) is used to force finalization (and you should do this during program development to speed debugging). But even if it isn't, it's highly probable that the errant Book will eventually be discovered through repeated executions of the program (assuming the program allocates enough storage to cause the garbage collector to execute).
4-3-4. How a garbage collector works▲
If you come from a programming language where allocating objects on the heap is expensive, you may naturally assume that Java's scheme of allocating everything (except primitives) on the heap is also expensive. However, it turns out that the garbage collector can have a significant impact on increasing the speed of object creation. This might sound a bit odd at first-that storage release affects storage allocation-but it's the way some JVMs work, and it means that allocating storage for heap objects in Java can be nearly as fast as creating storage on the stack in other languages.
For example, you can think of the C++ heap as a yard where each object stakes out its own piece of turf. This real estate can become abandoned sometime later and must be reused. In some JVMs, the Java heap is quite different; it's more like a conveyor belt that moves forward every time you allocate a new object. This means that object storage allocation is remarkably rapid. The "heap pointer" is simply moved forward into virgin territory, so it's effectively the same as C++'s stack allocation. (Of course, there's a little extra overhead for bookkeeping, but it's nothing like searching for storage.)
Now you might observe that the heap isn't in fact a conveyor belt, and if you treat it that way, you'll eventually start paging memory a lot (which is a big performance hit) and later run out. The trick is that the garbage collector steps in, and while it collects the garbage it compacts all the objects in the heap so that you've effectively moved the "heap pointer" closer to the beginning of the conveyor belt and farther away from a page fault. The garbage collector rearranges things and makes it possible for the high-speed, infinite-free-heap model to be used while allocating storage.
To understand how this works, you need to get a little better idea of the way different garbage collector (GC) schemes work. A simple but slow garbage collection technique is is called reference counting. This means that each object contains a reference counter, and every time a reference is attached to an object, the reference count is increased. Every time a reference goes out of scope or is set to null, the reference count is decreased. Thus, managing reference counts is a small but constant overhead that happens throughout the lifetime of your program. The garbage collector moves through the entire list of objects, and when it finds one with a reference count of zero it releases that storage. The one drawback is that if objects circularly refer to each other they can have nonzero reference counts while still being garbage. Locating such self-referential groups requires significant extra work for the garbage collector. Reference counting is commonly used to explain one kind of garbage collection, but it doesn't seem to be used in any JVM implementations.
In faster schemes, garbage collection is not based on reference counting. Instead, it is based on the idea that any nondead object must ultimately be traceable back to a reference that lives either on the stack or in static storage. The chain might go through several layers of objects. Thus, if you start in the stack and the static storage area and walk through all the references, you'll find all the live objects. For each reference that you find, you must trace into the object that it points to and then follow all the references in that object, tracing into the objects they point to, etc., until you've moved through the entire web that originated with the reference on the stack or in static storage. Each object that you move through must still be alive. Note that there is no problem with detached self-referential groups-these are simply not found, and are therefore automatically garbage.
In the approach described here, the JVM uses an adaptive garbage-collection scheme, and what it does with the live objects that it locates depends on the variant currently being used. One of these variants is stop-and-copy. This means that-for reasons that will become apparent-the program is first stopped (this is not a background collection scheme). Then, each live object that is found is copied from one heap to another, leaving behind all the garbage. In addition, as the objects are copied into the new heap, they are packed end-to-end, thus compacting the new heap (and allowing new storage to simply be reeled off the end as previously described).
Of course, when an object is moved from one place to another, all references that point at (i.e., that reference) the object must be changed. The reference that goes from the heap or the static storage area to the object can be changed right away, but there can be other references pointing to this object that will be encountered later during the "walk." These are fixed up as they are found (you could imagine a table that maps old addresses to new ones).
There are two issues that make these so-called "copy collectors" inefficient. The first is the idea that you have two heaps and you slosh all the memory back and forth between these two separate heaps, maintaining twice as much memory as you actually need. Some JVMs deal with this by allocating the heap in chunks as needed and simply copying from one chunk to another.
The second issue is the copying. Once your program becomes stable, it might be generating little or no garbage. Despite that, a copy collector will still copy all the memory from one place to another, which is wasteful. To prevent this, some JVMs detect that no new garbage is being generated and switch to a different scheme (this is the "adaptive" part). This other scheme is called mark-and-sweep, and it's what earlier versions of Sun's JVM used all the time. For general use, mark-and-sweep is fairly slow, but when you know you're generating little or no garbage, it's fast.
Mark-and-sweep follows the same logic of starting from the stack and static storage and tracing through all the references to find live objects. However, each time it finds a live object, that object is marked by setting a flag in it, but the object isn't collected yet. Only when the marking process is finished does the sweep occur. During the sweep, the dead objects are released. However, no copying happens, so if the collector chooses to compact a fragmented heap, it does so by shuffling objects around.
The "stop-and-copy" refers to the idea that this type of garbage collection is not done in the background; instead, the program is stopped while the garbage collection occurs. In the Sun literature you'll find many references to garbage collection as a low-priority background process, but it turns out that the garbage collection was not implemented that way, at least in earlier versions of the Sun JVM. Instead, the Sun garbage collector ran when memory got low. In addition, mark-and-sweep requires that the program be stopped.
As previously mentioned, in the JVM described here memory is allocated in big blocks. If you allocate a large object, it gets its own block. Strict stop-and-copy requires copying every live object from the source heap to a new heap before you could free the old one, which translates to lots of memory. With blocks, the garbage collection can typically copy objects to dead blocks as it collects. Each block has a generation count to keep track of whether it's alive. In the normal case, only the blocks created since the last garbage collection are compacted; all other blocks get their generation count bumped if they have been referenced from somewhere. This handles the normal case of lots of short-lived temporary objects. Periodically, a full sweep is made-large objects are still not copied (they just get their generation count bumped), and blocks containing small objects are copied and compacted. The JVM monitors the efficiency of garbage collection and if it becomes a waste of time because all objects are long-lived, then it switches to mark-and-sweep. Similarly, the JVM keeps track of how successful mark-and-sweep is, and if the heap starts to become fragmented, it switches back to stop-and-copy. This is where the "adaptive" part comes in, so you end up with a mouthful: "Adaptive generational stop-and-copy mark-and-sweep."
There are a number of additional speedups possible in a JVM. An especially important one involves the operation of the loader and what is called a just-in-time (JIT) compiler. A JIT compiler partially or fully converts a program into native machine code so that it doesn't need to be interpreted by the JVM and thus runs much faster. When a class must be loaded (typically, the first time you want to create an object of that class), the .class file is located, and the byte codes for that class are brought into memory. At this point, one approach is to simply JIT compile all the code, but this has two drawbacks: it takes a little more time, which, compounded throughout the life of the program, can add up; and it increases the size of the executable (byte codes are significantly more compact than expanded JIT code), and this might cause paging, which definitely slows down a program. An alternative approach is lazy evaluation, which means that the code is not JIT compiled until necessary. Thus, code that never gets executed might never be JIT compiled. The Java HotSpot technologies in recent JDKs take a similar approach by increasingly optimizing a piece of code each time it is executed, so the more the code is executed, the faster it gets.
4-4. Member initialization▲
Java goes out of its way to guarantee that variables are properly initialized before they are used. In the case of variables that are defined locally to a method, this guarantee comes in the form of a compile-time error. So if you say:
void
f
(
) {
int
i;
i++
; // Error -- i not initialized
}
you'll get an error message that says that i might not have been initialized. Of course, the compiler could have given i a default value, but it's more likely that this is a programmer error and a default value would have covered that up. Forcing the programmer to provide an initialization value is more likely to catch a bug.
If a primitive is a field in a class, however, things are a bit different. Since any method can initialize or use that data, it might not be practical to force the user to initialize it to its appropriate value before the data is used. However, it's unsafe to leave it with a garbage value, so each primitive field of a class is guaranteed to get an initial value. Those values can be seen here:
//: c04:InitialValues.java
// Shows default initial values.
import
com.bruceeckel.simpletest.*;
public
class
InitialValues {
static
Test monitor =
new
Test
(
);
boolean
t;
char
c;
byte
b;
short
s;
int
i;
long
l;
float
f;
double
d;
void
print
(
String s) {
System.out.println
(
s); }
void
printInitialValues
(
) {
print
(
"Data type Initial value"
);
print
(
"boolean "
+
t);
print
(
"char ["
+
c +
"]"
);
print
(
"byte "
+
b);
print
(
"short "
+
s);
print
(
"int "
+
i);
print
(
"long "
+
l);
print
(
"float "
+
f);
print
(
"double "
+
d);
}
public
static
void
main
(
String[] args) {
InitialValues iv =
new
InitialValues
(
);
iv.printInitialValues
(
);
/* You could also say:
new InitialValues().printInitialValues();
*/
monitor.expect
(
new
String[] {
"Data type Initial value"
,
"boolean false"
,
"char ["
+
(
char
)0
+
"]"
,
"byte 0"
,
"short 0"
,
"int 0"
,
"long 0"
,
"float 0.0"
,
"double 0.0"
}
);
}
}
///:~
You can see that even though the values are not specified, they automatically get initialized (The char value is a zero, which prints as a space). So at least there's no threat of working with uninitialized variables.
You'll see later that when you define an object reference inside a class without initializing it to a new object, that reference is given a special value of null (which is a Java keyword).
4-4-1. Specifying initialization▲
What happens if you want to give a variable an initial value? One direct way to do this is simply to assign the value at the point you define the variable in the class. (Notice you cannot do this in C++, although C++ novices always try.) Here the field definitions in class InitialValues are changed to provide initial values:
class
InitialValues {
boolean
b =
true
;
char
c =
'x'
;
byte
B =
47
;
short
s =
0xff
;
int
i =
999
;
long
l =
1
;
float
f =
3.14
f;
double
d =
3.14159
;
//. . .
You can also initialize nonprimitive objects in this same way. If Depth is a class, you can create a variable and initialize it like so:
class
Measurement {
Depth d =
new
Depth
(
);
// . . .
If you haven't given d an initial value and you try to use it anyway, you'll get a run-time error called an exception (covered in Chapter 9).
You can even call a method to provide an initialization value:
class
CInit {
int
i =
f
(
);
//...
}
This method can have arguments, of course, but those arguments cannot be other class members that haven't been initialized yet. Thus, you can do this:
class
CInit {
int
i =
f
(
);
int
j =
g
(
i);
//...
}
But you cannot do this:
class
CInit {
int
j =
g
(
i);
int
i =
f
(
);
//...
}
This is one place in which the compiler, appropriately, does complain about forward referencing, since this has to do with the order of initialization and not the way the program is compiled.
This approach to initialization is simple and straightforward. It has the limitation that every object of type InitialValues will get these same initialization values. Sometimes this is exactly what you need, but at other times you need more flexibility.
4-4-2. Constructor initialization▲
The constructor can be used to perform initialization, and this gives you greater flexibility in your programming because you can call methods and perform actions at run time to determine the initial values. There's one thing to keep in mind, however: You aren't precluding the automatic initialization, which happens before the constructor is entered. So, for example, if you say:
class
Counter {
int
i;
Counter
(
) {
i =
7
; }
// . . .
then i will first be initialized to 0, then to 7. This is true with all the primitive types and with object references, including those that are given explicit initialization at the point of definition. For this reason, the compiler doesn't try to force you to initialize elements in the constructor at any particular place, or before they are used-initialization is already guaranteed. (24)
4-4-2-1. Order of initialization▲
Within a class, the order of initialization is determined by the order that the variables are defined within the class. The variable definitions may be scattered throughout and in between method definitions, but the variables are initialized before any methods can be called-even the constructor. For example:
//: c04:OrderOfInitialization.java
// Demonstrates initialization order.
import
com.bruceeckel.simpletest.*;
// When the constructor is called to create a
// Tag object, you'll see a message:
class
Tag {
Tag
(
int
marker) {
System.out.println
(
"Tag("
+
marker +
")"
);
}
}
class
Card {
Tag t1 =
new
Tag
(
1
); // Before constructor
Card
(
) {
// Indicate we're in the constructor:
System.out.println
(
"Card()"
);
t3 =
new
Tag
(
33
); // Reinitialize t3
}
Tag t2 =
new
Tag
(
2
); // After constructor
void
f
(
) {
System.out.println
(
"f()"
);
}
Tag t3 =
new
Tag
(
3
); // At end
}
public
class
OrderOfInitialization {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
Card t =
new
Card
(
);
t.f
(
); // Shows that construction is done
monitor.expect
(
new
String[] {
"Tag(1)"
,
"Tag(2)"
,
"Tag(3)"
,
"Card()"
,
"Tag(33)"
,
"f()"
}
);
}
}
///:~
In Card, the definitions of the Tag objects are intentionally scattered about to prove that they'll all get initialized before the constructor is entered or anything else can happen. In addition, t3 is reinitialized inside the constructor.
From the output, you can see that, the t3 reference gets initialized twice: once before and once during the constructor call. (The first object is dropped, so it can be garbage collected later.) This might not seem efficient at first, but it guarantees proper initialization-what would happen if an overloaded constructor were defined that did not initialize t3 and there wasn't a "default" initialization for t3 in its definition?
4-4-2-2. Static data initialization▲
When the data is static, the same thing happens; if it's a primitive and you don't initialize it, it gets the standard primitive initial values. If it's a reference to an object, it's null unless you create a new object and attach your reference to it.
If you want to place initialization at the point of definition, it looks the same as for non-statics. There's only a single piece of storage for a static, regardless of how many objects are created. But the question arises of when the static storage gets initialized. An example makes this question clear:
//: c04:StaticInitialization.java
// Specifying initial values in a class definition.
import
com.bruceeckel.simpletest.*;
class
Bowl {
Bowl
(
int
marker) {
System.out.println
(
"Bowl("
+
marker +
")"
);
}
void
f
(
int
marker) {
System.out.println
(
"f("
+
marker +
")"
);
}
}
class
Table {
static
Bowl b1 =
new
Bowl
(
1
);
Table
(
) {
System.out.println
(
"Table()"
);
b2.f
(
1
);
}
void
f2
(
int
marker) {
System.out.println
(
"f2("
+
marker +
")"
);
}
static
Bowl b2 =
new
Bowl
(
2
);
}
class
Cupboard {
Bowl b3 =
new
Bowl
(
3
);
static
Bowl b4 =
new
Bowl
(
4
);
Cupboard
(
) {
System.out.println
(
"Cupboard()"
);
b4.f
(
2
);
}
void
f3
(
int
marker) {
System.out.println
(
"f3("
+
marker +
")"
);
}
static
Bowl b5 =
new
Bowl
(
5
);
}
public
class
StaticInitialization {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
System.out.println
(
"Creating new Cupboard() in main"
);
new
Cupboard
(
);
System.out.println
(
"Creating new Cupboard() in main"
);
new
Cupboard
(
);
t2.f2
(
1
);
t3.f3
(
1
);
monitor.expect
(
new
String[] {
"Bowl(1)"
,
"Bowl(2)"
,
"Table()"
,
"f(1)"
,
"Bowl(4)"
,
"Bowl(5)"
,
"Bowl(3)"
,
"Cupboard()"
,
"f(2)"
,
"Creating new Cupboard() in main"
,
"Bowl(3)"
,
"Cupboard()"
,
"f(2)"
,
"Creating new Cupboard() in main"
,
"Bowl(3)"
,
"Cupboard()"
,
"f(2)"
,
"f2(1)"
,
"f3(1)"
}
);
}
static
Table t2 =
new
Table
(
);
static
Cupboard t3 =
new
Cupboard
(
);
}
///:~
Bowl allows you to view the creation of a class, and Table and Cupboard create static members of Bowl scattered through their class definitions. Note that Cupboard creates a non-static Bowl b3 prior to the static definitions.
From the output, you can see that the static initialization occurs only if it's necessary. If you don't create a Table object and you never refer to Table.b1 or Table.b2, the static Bowl b1 and b2 will never be created. They are initialized only when the first Table object is created (or the first static access occurs). After that, the static objects are not reinitialized.
The order of initialization is statics first, if they haven't already been initialized by a previous object creation, and then the non-static objects. You can see the evidence of this in the output.
It's helpful to summarize the process of creating an object. Consider a class called Dog:
- The first time an object of type Dog is created (the constructor is actually a static method), or the first time a static method or static field of class Dog is accessed, the Java interpreter must locate Dog.class, which it does by searching through the classpath.
- As Dog.class is loaded (creating a Class object, which you'll learn about later), all of its static initializers are run. Thus, static initialization takes place only once, as the Class object is loaded for the first time.
- When you create a new Dog( ), the construction process for a Dog object first allocates enough storage for a Dog object on the heap.
- This storage is wiped to zero, automatically setting all the primitives in that Dog object to their default values (zero for numbers and the equivalent for boolean and char) and the references to null.
- Any initializations that occur at the point of field definition are executed.
- Constructors are executed. As you shall see in Chapter 6, this might actually involve a fair amount of activity, especially when inheritance is involved.
4-4-2-3. Explicit static initialization▲
Java allows you to group other static initializations inside a special "static clause" (sometimes called a static block)in a class. It looks like this:
class
Spoon {
static
int
i;
static
{
i =
47
;
}
// . . .
It appears to be a method, but it's just the static keyword followed by a block of code. This code, like other static initializations, is executed only once: the first time you make an object of that class or the first time you access a static member of that class (even if you never make an object of that class). For example:
//: c04:ExplicitStatic.java
// Explicit static initialization with the "static" clause.
import
com.bruceeckel.simpletest.*;
class
Cup {
Cup
(
int
marker) {
System.out.println
(
"Cup("
+
marker +
")"
);
}
void
f
(
int
marker) {
System.out.println
(
"f("
+
marker +
")"
);
}
}
class
Cups {
static
Cup c1;
static
Cup c2;
static
{
c1 =
new
Cup
(
1
);
c2 =
new
Cup
(
2
);
}
Cups
(
) {
System.out.println
(
"Cups()"
);
}
}
public
class
ExplicitStatic {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
System.out.println
(
"Inside main()"
);
Cups.c1.f
(
99
); // (1)
monitor.expect
(
new
String[] {
"Inside main()"
,
"Cup(1)"
,
"Cup(2)"
,
"f(99)"
}
);
}
// static Cups x = new Cups(); // (2)
// static Cups y = new Cups(); // (2)
}
///:~
The static initializers for Cups run when either the access of the static object c1 occurs on the line marked (1), or if line (1) is commented out and the lines marked (2) are uncommented. If both (1) and (2) are commented out, the static initialization for Cups never occurs. Also, it doesn't matter if one or both of the lines marked (2) are uncommented; the static initialization only occurs once.
4-4-2-4. Non-static instance initialization▲
Java provides a similar syntax for initializing non-static variables for each object. Here's an example:
//: c04:Mugs.java
// Java "Instance Initialization."
import
com.bruceeckel.simpletest.*;
class
Mug {
Mug
(
int
marker) {
System.out.println
(
"Mug("
+
marker +
")"
);
}
void
f
(
int
marker) {
System.out.println
(
"f("
+
marker +
")"
);
}
}
public
class
Mugs {
static
Test monitor =
new
Test
(
);
Mug c1;
Mug c2;
{
c1 =
new
Mug
(
1
);
c2 =
new
Mug
(
2
);
System.out.println
(
"c1 & c2 initialized"
);
}
Mugs
(
) {
System.out.println
(
"Mugs()"
);
}
public
static
void
main
(
String[] args) {
System.out.println
(
"Inside main()"
);
Mugs x =
new
Mugs
(
);
monitor.expect
(
new
String[] {
"Inside main()"
,
"Mug(1)"
,
"Mug(2)"
,
"c1 & c2 initialized"
,
"Mugs()"
}
);
}
}
///:~
You can see that the instance initialization clause:
{
c1 =
new
Mug
(
1
);
c2 =
new
Mug
(
2
);
System.out.println
(
"c1 & c2 initialized"
);
}
looks exactly like the static initialization clause except for the missing static keyword. This syntax is necessary to support the initialization of anonymous inner classes (see Chapter 8).
4-5. Array initialization▲
Initializing arrays in C is error-prone and tedious. C++ uses aggregate initialization to make it much safer. (25) Java has no "aggregates" like C++ does, since everything is an object in Java. It does have arrays, and these are supported with array initialization.
An array is simply a sequence of either objects or primitives that are all the same type and packaged together under one identifier name. Arrays are defined and used with the square-brackets indexing operator [ ]. To define an array, you simply follow your type name with empty square brackets:
int
[] a1;
You can also put the square brackets after the identifier to produce exactly the same meaning:
int
a1[];
This conforms to expectations from C and C++ programmers. The former style, however, is probably a more sensible syntax, since it says that the type is "an int array." That style will be used in this book.
The compiler doesn't allow you to tell it how big the array is. This brings us back to that issue of "references." All that you have at this point is a reference to an array, and there's been no space allocated for the array. To create storage for the array, you must write an initialization expression. For arrays, initialization can appear anywhere in your code, but you can also use a special kind of initialization expression that must occur at the point where the array is created. This special initialization is a set of values surrounded by curly braces. The storage allocation (the equivalent of using new) is taken care of by the compiler in this case. For example:
int
[] a1 =
{
1
, 2
, 3
, 4
, 5
}
;
So why would you ever define an array reference without an array?
int
[] a2;
Well, it's possible to assign one array to another in Java, so you can say:
a2 =
a1;
What you're really doing is copying a reference, as demonstrated here:
//: c04:Arrays.java
// Arrays of primitives.
import
com.bruceeckel.simpletest.*;
public
class
Arrays {
static
Test monitor =
new
Test
(
);
public
static
void
main
(
String[] args) {
int
[] a1 =
{
1
, 2
, 3
, 4
, 5
}
;
int
[] a2;
a2 =
a1;
for
(
int
i =
0
; i <
a2.length; i++
)
a2[i]++
;
for
(
int
i =
0
; i <
a1.length; i++
)
System.out.println
(
"a1["
+
i +
"] = "
+
a1[i]);
monitor.expect
(
new
String[] {
"a1[0] = 2"
,
"a1[1] = 3"
,
"a1[2] = 4"
,
"a1[3] = 5"
,
"a1[4] = 6"
}
);
}
}
///:~
You can see that a1 is given an initialization value but a2 is not; a2 is assigned later-in this case, to another array.
There's something new here: All arrays have an intrinsic member (whether they're arrays of objects or arrays of primitives) that you can query-but not change-to tell you how many elements there are in the array. This member is length. Since arrays in Java, like C and C++, start counting from element zero, the largest element you can index is length - 1. If you go out of bounds, C and C++ quietly accept this and allow you to stomp all over your memory, which is the source of many infamous bugs. However, Java protects you against such problems by causing a run-time error (an exception, the subject of Chapter 9) if you step out of bounds. Of course, checking every array access costs time and code and there's no way to turn it off, which means that array accesses might be a source of inefficiency in your program if they occur at a critical juncture. For Internet security and programmer productivity, the Java designers thought that this was a worthwhile trade-off.
What if you don't know how many elements you're going to need in your array while you're writing the program? You simply use new to create the elements in the array. Here, new works even though it's creating an array of primitives (new won't create a nonarray primitive):
//: c04:ArrayNew.java
// Creating arrays with new.
import
com.bruceeckel.simpletest.*;
import
java.util.*;
public
class
ArrayNew {
static
Test monitor =
new
Test
(
);
static
Random rand =
new
Random
(
);
public
static
void
main
(
String[] args) {
int
[] a;
a =
new
int
[rand.nextInt
(
20
)];
System.out.println
(
"length of a = "
+
a.length);
for
(
int
i =
0
; i <
a.length; i++
)
System.out.println
(
"a["
+
i +
"] = "
+
a[i]);
monitor.expect
(
new
Object[] {
"%% length of a =
\\
d+"
,
new
TestExpression
(
"%% a
\\
[
\\
d+
\\
] = 0"
, a.length)
}
);
}
}
///:~
The expect( ) statement contains something new in this example: the TestExpression class. A TestExpression object takes an expression, either an ordinary stringor a regular expression as shown here, and a second integer argument that indicates that the preceding expression will be repeated that many times. TestExpression not only prevents needless duplication in the code, but in this case, it allows the number of repetitions to be determined at run time.
The size of the array is chosen at random by using the Random.nextInt( ) method, which produces a value from zero to that of its argument. Because of the randomness, it's clear that array creation is actually happening at run time. In addition, the output of this program shows that array elements of primitive types are automatically initialized to "empty" values. (For numerics and char, this is zero, and for boolean, it's false.)
Of course, the array could also have been defined and initialized in the same statement:
int
[] a =
new
int
[rand.nextInt
(
20
)];
This is the preferred way to do it, if you can.
If you're dealing with an array of nonprimitive objects, you must always use new. Here, the reference issue comes up again, because what you create is an array of references. Consider the wrapper type Integer, which is a class and not a primitive:
//: c04:ArrayClassObj.java
// Creating an array of nonprimitive objects.
import
com.bruceeckel.simpletest.*;
import
java.util.*;
public
class
ArrayClassObj {
static
Test monitor =
new
Test
(
);
static
Random rand =
new
Random
(
);
public
static
void
main
(
String[] args) {
Integer[] a =
new
Integer[rand.nextInt
(
20
)];
System.out.println
(
"length of a = "
+
a.length);
for
(
int
i =
0
; i <
a.length; i++
) {
a[i] =
new
Integer
(
rand.nextInt
(
500
));
System.out.println
(
"a["
+
i +
"] = "
+
a[i]);
}
monitor.expect
(
new
Object[] {
"%% length of a =
\\
d+"
,
new
TestExpression
(
"%% a
\\
[
\\
d+
\\
] =
\\
d+"
, a.length)
}
);
}
}
///:~
Here, even after new is called to create the array:
Integer[] a =
new
Integer[rand.nextInt
(
20
)];
it's only an array of references, and not until the reference itself is initialized by creating a new Integer object is the initialization complete:
a[i] =
new
Integer
(
rand.nextInt
(
500
));
If you forget to create the object, however, you'll get an exception at run time when you try to use the empty array location.
Take a look at the formation of the String object inside the print statements. You can see that the reference to the Integer object is automatically converted to produce a String representing the value inside the object.
It's also possible to initialize arrays of objects by using the curly-brace-enclosed list. There are two forms:
//: c04:ArrayInit.java
// Array initialization.
public
class
ArrayInit {
public
static
void
main
(
String[] args) {
Integer[] a =
{
new
Integer
(
1
),
new
Integer
(
2
),
new
Integer
(
3
),
}
;
Integer[] b =
new
Integer[] {
new
Integer
(
1
),
new
Integer
(
2
),
new
Integer
(
3
),
}
;
}
}
///:~
The first form is useful at times, but it's more limited since the size of the array is determined at compile time. The final comma in the list of initializers is optional. (This feature makes for easier maintenance of long lists.)
The second form provides a convenient syntax to create and call methods that can produce the same effect as C's variable argument lists (known as "varargs" in C). These can include unknown quantities of arguments as well as unknown types. Since all classes are ultimately inherited from the common root class Object (a subject you will learn more about as this book progresses), you can create a method that takes an array of Object and call it like this:
//: c04:VarArgs.java
// Using array syntax to create variable argument lists.
import
com.bruceeckel.simpletest.*;
class
A {
int
i; }
public
class
VarArgs {
static
Test monitor =
new
Test
(
);
static
void
print
(
Object[] x) {
for
(
int
i =
0
; i <
x.length; i++
)
System.out.println
(
x[i]);
}
public
static
void
main
(
String[] args) {
print
(
new
Object[] {
new
Integer
(
47
), new
VarArgs
(
),
new
Float
(
3.14
), new
Double
(
11.11
)
}
);
print
(
new
Object[] {
"one"
, "two"
, "three"
}
);
print
(
new
Object[] {
new
A
(
), new
A
(
), new
A
(
)}
);
monitor.expect
(
new
Object[] {
"47"
,
"%% VarArgs@
\\
p{XDigit}+"
,
"3.14"
,
"11.11"
,
"one"
,
"two"
,
"three"
,
new
TestExpression
(
"%% A@
\\
p{XDigit}+"
, 3
)
}
);
}
}
///:~
You can see that print( ) takes an array of Object, then steps through the array and prints each one. The standard Java library classes produce sensible output, but the objects of the classes created here-A and VarArgs-print the class name, followed by an '@' sign, and yet another regular expression construct, \p{XDigit}, which indicates a hexadecimal digit. The trailing '+' means there will be one or more hexadecimal digits. Thus, the default behavior (if you don't define a toString( ) method for your class, which will be described later in the book) is to print the class name and the address of the object.
4-5-1. Multidimensional arrays▲
Java allows you to easily create multidimensional arrays:
//: c04:MultiDimArray.java
// Creating multidimensional arrays.
import
com.bruceeckel.simpletest.*;
import
java.util.*;
public
class
MultiDimArray {
static
Test monitor =
new
Test
(
);
static
Random rand =
new
Random
(
);
public
static
void
main
(
String[] args) {
int
[][] a1 =
{
{
1
, 2
, 3
, }
,
{
4
, 5
, 6
, }
,
}
;
for
(
int
i =
0
; i <
a1.length; i++
)
for
(
int
j =
0
; j <
a1[i].length; j++
)
System.out.println
(
"a1["
+
i +
"]["
+
j +
"] = "
+
a1[i][j]);
// 3-D array with fixed length:
int
[][][] a2 =
new
int
[2
][2
][4
];
for
(
int
i =
0
; i <
a2.length; i++
)
for
(
int
j =
0
; j <
a2[i].length; j++
)
for
(
int
k =
0
; k <
a2[i][j].length; k++
)
System.out.println
(
"a2["
+
i +
"]["
+
j +
"]["
+
k +
"] = "
+
a2[i][j][k]);
// 3-D array with varied-length vectors:
int
[][][] a3 =
new
int
[rand.nextInt
(
7
)][][];
for
(
int
i =
0
; i <
a3.length; i++
) {
a3[i] =
new
int
[rand.nextInt
(
5
)][];
for
(
int
j =
0
; j <
a3[i].length; j++
)
a3[i][j] =
new
int
[rand.nextInt
(
5
)];
}
for
(
int
i =
0
; i <
a3.length; i++
)
for
(
int
j =
0
; j <
a3[i].length; j++
)
for
(
int
k =
0
; k <
a3[i][j].length; k++
)
System.out.println
(
"a3["
+
i +
"]["
+
j +
"]["
+
k +
"] = "
+
a3[i][j][k]);
// Array of nonprimitive objects:
Integer[][] a4 =
{
{
new
Integer
(
1
), new
Integer
(
2
)}
,
{
new
Integer
(
3
), new
Integer
(
4
)}
,
{
new
Integer
(
5
), new
Integer
(
6
)}
,
}
;
for
(
int
i =
0
; i <
a4.length; i++
)
for
(
int
j =
0
; j <
a4[i].length; j++
)
System.out.println
(
"a4["
+
i +
"]["
+
j +
"] = "
+
a4[i][j]);
Integer[][] a5;
a5 =
new
Integer[3
][];
for
(
int
i =
0
; i <
a5.length; i++
) {
a5[i] =
new
Integer[3
];
for
(
int
j =
0
; j <
a5[i].length; j++
)
a5[i][j] =
new
Integer
(
i *
j);
}
for
(
int
i =
0
; i <
a5.length; i++
)
for
(
int
j =
0
; j <
a5[i].length; j++
)
System.out.println
(
"a5["
+
i +
"]["
+
j +
"] = "
+
a5[i][j]);
// Output test
int
ln =
0
;
for
(
int
i =
0
; i <
a3.length; i++
)
for
(
int
j =
0
; j <
a3[i].length; j++
)
for
(
int
k =
0
; k <
a3[i][j].length; k++
)
ln++
;
monitor.expect
(
new
Object[] {
"a1[0][0] = 1"
,
"a1[0][1] = 2"
,
"a1[0][2] = 3"
,
"a1[1][0] = 4"
,
"a1[1][1] = 5"
,
"a1[1][2] = 6"
,
new
TestExpression
(
"%% a2
\\
[
\\
d
\\
]
\\
[
\\
d
\\
]
\\
[
\\
d
\\
] = 0"
, 16
),
new
TestExpression
(
"%% a3
\\
[
\\
d
\\
]
\\
[
\\
d
\\
]
\\
[
\\
d
\\
] = 0"
, ln),
"a4[0][0] = 1"
,
"a4[0][1] = 2"
,
"a4[1][0] = 3"
,
"a4[1][1] = 4"
,
"a4[2][0] = 5"
,
"a4[2][1] = 6"
,
"a5[0][0] = 0"
,
"a5[0][1] = 0"
,
"a5[0][2] = 0"
,
"a5[1][0] = 0"
,
"a5[1][1] = 1"
,
"a5[1][2] = 2"
,
"a5[2][0] = 0"
,
"a5[2][1] = 2"
,
"a5[2][2] = 4"
}
);
}
}
///:~
The code used for printing uses length so that it doesn't depend on fixed array sizes.
The first example shows a multidimensional array of primitives. You delimit each vector in the array by using curly braces:
int
[][] a1 =
{
{
1
, 2
, 3
, }
,
{
4
, 5
, 6
, }
,
}
;
Each set of square brackets moves you into the next level of the array.
The second example shows a three-dimensional array allocated with new. Here, the whole array is allocated at once:
int
[][][] a2 =
new
int
[2
][2
][4
];
But the third example shows that each vector in the arrays that make up the matrix can be of any length:
int
[][][] a3 =
new
int
[rand.nextInt
(
7
)][][];
for
(
int
i =
0
; i <
a3.length; i++
) {
a3[i] =
new
int
[rand.nextInt
(
5
)][];
for
(
int
j =
0
; j <
a3[i].length; j++
)
a3[i][j] =
new
int
[rand.nextInt
(
5
)];
}
The first new creates an array with a random-length first element and the rest undetermined. The second new inside the for loop fills out the elements but leaves the third index undetermined until you hit the third new.
You will see from the output that array values are automatically initialized to zero if you don't give them an explicit initialization value.
You can deal with arrays of nonprimitive objects in a similar fashion, which is shown in the fourth example, demonstrating the ability to collect many new expressions with curly braces:
Integer[][] a4 =
{
{
new
Integer
(
1
), new
Integer
(
2
)}
,
{
new
Integer
(
3
), new
Integer
(
4
)}
,
{
new
Integer
(
5
), new
Integer
(
6
)}
,
}
;
The fifth example shows how an array of nonprimitive objects can be built up piece by piece:
Integer[][] a5;
a5 =
new
Integer[3
][];
for
(
int
i =
0
; i <
a5.length; i++
) {
a5[i] =
new
Integer[3
];
for
(
int
j =
0
; j <
a5[i].length; j++
)
a5[i][j] =
new
Integer
(
i*
j);
}
The i*j is just to put an interesting value into the Integer.
4-6. Summary▲
This seemingly elaborate mechanism for initialization, the constructor, should give you a strong hint about the critical importance placed on initialization in the language. As Bjarne Stroustrup, the inventor of C++, was designing that language, one of the first observations he made about productivity in C was that improper initialization of variables causes a significant portion of programming problems. These kinds of bugs are hard to find, and similar issues apply to improper cleanup. Because constructors allow you to guarantee proper initialization and cleanup (the compiler will not allow an object to be created without the proper constructor calls), you get complete control and safety.
In C++, destruction is quite important because objects created with new must be explicitly destroyed. In Java, the garbage collector automatically releases the memory for all objects, so the equivalent cleanup method in Java isn't necessary much of the time (but when it is, as observed in this chapter, you must do it yourself). In cases where you don't need destructor-like behavior, Java's garbage collector greatly simplifies programming and adds much-needed safety in managing memory. Some garbage collectors can even clean up other resources like graphics and file handles. However, the garbage collector does add a run-time cost, the expense of which is difficult to put into perspective because of the historical slowness of Java interpreters. Although Java has had significant performance increases over time, the speed problem has taken its toll on the adoption of the language for certain types of programming problems.
Because of the guarantee that all objects will be constructed, there's actually more to the constructor than what is shown here. In particular, when you create new classes using either composition or inheritance, the guarantee of construction also holds, and some additional syntax is necessary to support this. You'll learn about composition, inheritance, and how they affect constructors in future chapters.
4-7. Exercises▲
Solutions to selected exercises can be found in the electronic document The Thinking in Java Annotated Solution Guide, available for a small fee from www.BruceEckel.com.
- Create a class with a default constructor (one that takes no arguments) that prints a message. Create an object of this class.
- Add an overloaded constructor to Exercise 1 that takes a String argument and prints it along with your message.
- Create an array of object references of the class you created in Exercise 2, but don't actually create objects to assign into the array. When you run the program, notice whether the initialization messages from the constructor calls are printed.
- Complete Exercise 3 by creating objects to attach to the array of references.
- Create an array of String objects and assign a string to each element. Print the array by using a for loop.
- Create a class called Dog with an overloaded bark( ) method. This method should be overloaded based on various primitive data types, and print different types of barking, howling, etc., depending on which overloaded version is called. Write a main( ) that calls all the different versions.
- Modify Exercise 6 so that two of the overloaded methods have two arguments (of two different types), but in reversed order relative to each other. Verify that this works.
- Create a class without a constructor, and then create an object of that class in main( ) to verify that the default constructor is automatically synthesized.
- Create a class with two methods. Within the first method, call the second method twice: the first time without using this, and the second time using this.
- Create a class with two (overloaded) constructors. Using this, call the second constructor inside the first one.
- Create a class with a finalize( ) method that prints a message. In main( ), create an object of your class. Explain the behavior of your program.
- Modify Exercise 11 so that your finalize( ) will always be called.
- Create a class called Tank that can be filled and emptied, and has a termination condition that it must be empty when the object is cleaned up. Write a finalize( ) that verifies this termination condition. In main( ), test the possible scenarios that can occur when your Tank is used.
- Create a class containing an int and a char that are not initialized, and print their values to verify that Java performs default initialization.
- Create a class containing an uninitialized String reference. Demonstrate that this reference is initialized by Java to null.
- Create a class with a String field that is initialized at the point of definition, and another one that is initialized by the constructor. What is the difference between the two approaches?
- Create a class with a static String field that is initialized at the point of definition, and another one that is initialized by the static block. Add a static method that prints both fields and demonstrates that they are both initialized before they are used.
- Create a class with a String that is initialized using "instance initialization." Describe a use for this feature (other than the one specified in this book).
- Write a method that creates and initializes a two-dimensional array of double. The size of the array is determined by the arguments of the method, and the initialization values are a range determined by beginning and ending values that are also arguments of the method. Create a second method that will print the array generated by the first method. In main( ) test the methods by creating and printing several different sizes of arrays.
- Repeat Exercise 19 for a three-dimensional array.
- Comment the line marked (1) in ExplicitStatic.java and verify that the static initialization clause is not called. Now uncomment one of the lines marked (2) and verify that the static initialization clause is called. Now uncomment the other line marked (2) and verify that static initialization only occurs once.