ZeePedia

Hiding the Implementation:the library unit, Java access specifiers, Interface and implementation

<< Initialization & Cleanup:Method overloading, Member initialization
Reusing Classes:Composition syntax, Combining composition and inheritance >>
img
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 further 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 the different garbage collector (GC) schemes work. A simple but slow
GC technique is 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
216
Thinking in Java
img
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
Chapter 4: Initialization & Cleanup
217
img
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
GC 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
GC 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 GC can typically use dead blocks to copy objects
to 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 GC are compacted; all other blocks get their generation count bumped
if they have been referenced from somewhere. This handles the normal
218
Thinking in Java
img
case of lots of short-lived temporary objects. Periodically, a full sweep is
made--large objects are still not copied (just get their generation count
bumped) and blocks containing small objects are copied and compacted.
The JVM monitors the efficiency of GC 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 Just-In-
Time (JIT) compiler. 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 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 get JIT
compiled.
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++;
}
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
Chapter 4: Initialization & Cleanup
219
img
covered that up. Forcing the programmer to provide an initialization
value is more likely to catch a bug.
If a primitive is a data member of 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 data member of a class is guaranteed to get an initial value.
Those values can be seen here:
//: c04:InitialValues.java
// Shows default initial values.
class Measurement {
boolean t;
char c;
byte b;
short s;
int i;
long l;
float f;
double d;
void print() {
System.out.println(
"Data type
Initial value\n" +
"boolean
" + t + "\n" +
"char
[" + c + "] "+ (int)c +"\n"+
"byte
" + b + "\n" +
"short
" + s + "\n" +
"int
" + i + "\n" +
"long
" + l + "\n" +
"float
" + f + "\n" +
"double
" + d);
}
}
public class InitialValues {
public static void main(String[] args) {
Measurement d = new Measurement();
d.print();
/* In this case you could also say:
220
Thinking in Java
img
new Measurement().print();
*/
}
} ///:~
The output of this program is:
Data type
Initial value
boolean
false
char
[]0
byte
0
short
0
int
0
long
0
float
0.0
double
0.0
The char value is a zero, which prints as a space.
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).
You can see that even though the values are not specified, they
automatically get initialized. So at least there's no threat of working with
uninitialized variables.
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 Measurement are
changed to provide initial values:
class Measurement {
boolean b = true;
char c = 'x';
byte B = 47;
short s = 0xff;
int i = 999;
long l = 1;
float f = 3.14f;
Chapter 4: Initialization & Cleanup
221
img
double d = 3.14159;
//. . .
You can also initialize nonprimitive objects in this same way. If Depth is
a class, you can insert a variable and initialize it like so:
class Measurement {
Depth o = new Depth();
boolean b = true;
// . . .
If you haven't given o an initial value and you try to use it anyway, you'll
get a run-time error called an exception (covered in Chapter 10).
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.
222
Thinking in Java
img
This approach to initialization is simple and straightforward. It has the
limitation that every object of type Measurement will get these same
initialization values. Sometimes this is exactly what you need, but at other
times you need more flexibility.
Constructor initialization
The constructor can be used to perform initialization, and this gives you
greater flexibility in your programming since 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
guaranteed4.
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.
4 In contrast, C++ has the constructor initializer list that causes initialization to occur
before entering the constructor body, and is enforced for objects. See Thinking in C++, 2nd
edition (available on this book's CD ROM and at ).
Chapter 4: Initialization & Cleanup
223
img
// 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 {
public static void main(String[] args) {
Card t = new Card();
t.f(); // Shows that construction is done
}
} ///:~
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. The output is:
Tag(1)
Tag(2)
Tag(3)
Card()
Tag(33)
f()
224
Thinking in Java
img
Thus, 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?
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.
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 + ")");
Chapter 4: Initialization & Cleanup
225
img
}
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 {
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);
}
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. The output shows what happens:
Bowl(1)
Bowl(2)
Table()
f(1)
226
Thinking in Java
img
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)
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. However, 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:
1.
The first time an object of type Dog is created, 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.
2.
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.
Chapter 4: Initialization & Cleanup
227
img
3.
When you create a new Dog( ), the construction process for a
Dog object first allocates enough storage for a Dog object on the
heap.
4.
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.
5.
Any initializations that occur at the point of field definition are
executed.
6.
Constructors are executed. As you shall see in Chapter 6, this might
actually involve a fair amount of activity, especially when
inheritance is involved.
Explicit static initialization
Java allows you to group other static initializations inside a special
"static construction 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
method body. 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.
class Cup {
Cup(int marker) {
System.out.println("Cup(" + marker + ")");
228
Thinking in Java
img
}
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 {
public static void main(String[] args) {
System.out.println("Inside main()");
Cups.c1.f(99);  // (1)
}
// 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.
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."
Chapter 4: Initialization & Cleanup
229
img
class Mug {
Mug(int marker) {
System.out.println("Mug(" + marker + ")");
}
void f(int marker) {
System.out.println("f(" + marker + ")");
}
}
public class Mugs {
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();
}
} ///:~
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).
230
Thinking in Java
img
Array initialization
Initializing arrays in C is error-prone and tedious. C++ uses aggregate
initialization to make it much safer5. Java has no "aggregates" like C++,
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, 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?
5 See Thinking in C++, 2nd edition for a complete description of C++ aggregate
initialization.
Chapter 4: Initialization & Cleanup
231
img
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.
public class Arrays {
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]);
}
} ///:~
You can see that a1 is given an initialization value while 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 10) 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.
232
Thinking in Java
img
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 java.util.*;
public class ArrayNew {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
int[] a;
a = new int[pRand(20)];
System.out.println(
"length of a = " + a.length);
for(int i = 0; i < a.length; i++)
System.out.println(
"a[" + i + "] = " + a[i]);
}
} ///:~
Since the size of the array is chosen at random (using the pRand( )
method), it's clear that array creation is actually happening at run-time.
In addition, you'll see from the output of this program 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[pRand(20)];
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
Chapter 4: Initialization & Cleanup
233
img
// Creating an array of nonprimitive objects.
import java.util.*;
public class ArrayClassObj {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
public static void main(String[] args) {
Integer[] a = new Integer[pRand(20)];
System.out.println(
"length of a = " + a.length);
for(int i = 0; i < a.length; i++) {
a[i] = new Integer(pRand(500));
System.out.println(
"a[" + i + "] = " + a[i]);
}
}
} ///:~
Here, even after new is called to create the array:
Integer[] a = new Integer[pRand(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(pRand(500));
If you forget to create the object, however, you'll get an exception at run-
time when you try to read 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 using the curly-brace-
enclosed list. There are two forms:
//: c04:ArrayInit.java
// Array initialization.
234
Thinking in Java
img
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),
};
}
} ///:~
This 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 of array initialization 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
quantity 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 the array syntax to create
// variable argument lists.
class A { int i; }
public class VarArgs {
static void f(Object[] x) {
for(int i = 0; i < x.length; i++)
System.out.println(x[i]);
}
public static void main(String[] args) {
f(new Object[] {
Chapter 4: Initialization & Cleanup
235
img
new Integer(47), new VarArgs(),
new Float(3.14), new Double(11.11) });
f(new Object[] {"one", "two", "three" });
f(new Object[] {new A(), new A(), new A()});
}
} ///:~
At this point, there's not much you can do with these unknown objects,
and this program uses the automatic String conversion to do something
useful with each Object. In Chapter 12, which covers run-time type
identification (RTTI), you'll learn how to discover the exact type of such
objects so that you can do something more interesting with them.
Multidimensional arrays
Java allows you to easily create multidimensional arrays:
//: c04:MultiDimArray.java
// Creating multidimensional arrays.
import java.util.*;
public class MultiDimArray {
static Random rand = new Random();
static int pRand(int mod) {
return Math.abs(rand.nextInt()) % mod + 1;
}
static void prt(String s) {
System.out.println(s);
}
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++)
prt("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++)
236
Thinking in Java
img
for(int j = 0; j < a2[i].length; j++)
for(int k = 0; k < a2[i][j].length;
k++)
prt("a2[" + i + "][" +
j + "][" + k +
"] = " + a2[i][j][k]);
// 3-D array with varied-length vectors:
int[][][] a3 = new int[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(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++)
prt("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++)
prt("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++)
prt("a5[" + i + "][" + j +
"] = " + a5[i][j]);
Chapter 4: Initialization & Cleanup
237
img
}
} ///:~
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 with 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[pRand(7)][][];
for(int i = 0; i < a3.length; i++) {
a3[i] = new int[pRand(5)][];
for(int j = 0; j < a3[i].length; j++)
a3[i][j] = new int[pRand(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 = {
238
Thinking in Java
img
{ 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.
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 Stroustrup was designing C++, 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. 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 overall
Chapter 4: Initialization & Cleanup
239
img
slowness of Java interpreters at this writing. As this changes, we'll be able
to discover if the overhead of the garbage collector will preclude the use of
Java for certain types of programs. (One of the issues is the
unpredictability of the garbage collector.)
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.
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 .
1.
Create a class with a default constructor (one that takes no
arguments) that prints a message. Create an object of this class.
2.
Add an overloaded constructor to Exercise 1 that takes a String
argument and prints it along with your message.
3.
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.
4.
Complete Exercise 3 by creating objects to attach to the array of
references.
5.
Create an array of String objects and assign a string to each
element. Print the array using a for loop.
6.
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.
240
Thinking in Java
img
7.
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.
8.
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.
9.
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.
10.
Create a class with two (overloaded) constructors. Using this, call
the second constructor inside the first one.
11.
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.
12.
Modify Exercise 11 so that your finalize( ) will always be called.
13.
Create a class called Tank that can be filled and emptied, and has
a death condition that it must be empty when the object is cleaned
up. Write a finalize( ) that verifies this death condition. In
main( ), test the possible scenarios that can occur when your
Tank is used.
14.
Create a class containing an int and a char that are not initialized,
and print their values to verify that Java performs default
initialization.
15.
Create a class containing an uninitialized String reference.
Demonstrate that this reference is initialized by Java to null.
16.
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?
17.
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
Chapter 4: Initialization & Cleanup
241
img
block. Add a static method that prints both fields and
demonstrates that they are both initialized before they are used.
18.
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).
19.
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.
20.
Repeat Exercise 19 for a three-dimensional array.
21.
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.
22.
Experiment with Garbage.java by running the program using
the arguments "gc," "finalize," or "all." Repeat the process and see
if you detect any patterns in the output. Change the code so that
System.runFinalization( ) is called before System.gc( ) and
observe the results.
242
Thinking in Java
img
5: Hiding the
Implementation
A primary consideration in object-oriented design is
"separating the things that change from the things that
stay the same."
This is particularly important for libraries. The user (client programmer)
of that library must be able to rely on the part they use, and know that
they won't need to rewrite code if a new version of the library comes out.
On the flip side, the library creator must have the freedom to make
modifications and improvements with the certainty that the client
programmer's code won't be affected by those changes.
This can be achieved through convention. For example, the library
programmer must agree to not remove existing methods when modifying
a class in the library, since that would break the client programmer's code.
The reverse situation is thornier, however. In the case of a data member,
how can the library creator know which data members have been accessed
by client programmers? This is also true with methods that are only part
of the implementation of a class, and not meant to be used directly by the
client programmer. But what if the library creator wants to rip out an old
implementation and put in a new one? Changing any of those members
might break a client programmer's code. Thus the library creator is in a
strait jacket and can't change anything.
To solve this problem, Java provides access specifiers to allow the library
creator to say what is available to the client programmer and what is not.
The levels of access control from "most access" to "least access" are
public, protected, "friendly" (which has no keyword), and private.
From the previous paragraph you might think that, as a library designer,
you'll want to keep everything as "private" as possible, and expose only
the methods that you want the client programmer to use. This is exactly
right, even though it's often counterintuitive for people who program in
243
Table of Contents:
  1. Introduction to Objects:The progress of abstraction, An object has an interface
  2. Everything is an Object:You manipulate objects with references, Your first Java program
  3. Controlling Program Flow:Using Java operators, Execution control, true and false
  4. Initialization & Cleanup:Method overloading, Member initialization
  5. Hiding the Implementation:the library unit, Java access specifiers, Interface and implementation
  6. Reusing Classes:Composition syntax, Combining composition and inheritance
  7. Polymorphism:Upcasting revisited, The twist, Designing with inheritance
  8. Interfaces & Inner Classes:Extending an interface with inheritance, Inner class identifiers
  9. Holding Your Objects:Container disadvantage, List functionality, Map functionality
  10. Error Handling with Exceptions:Basic exceptions, Catching an exception
  11. The Java I/O System:The File class, Compression, Object serialization, Tokenizing input
  12. Run-time Type Identification:The need for RTTI, A class method extractor
  13. Creating Windows & Applets:Applet restrictions, Running applets from the command line
  14. Multiple Threads:Responsive user interfaces, Sharing limited resources, Runnable revisited
  15. Distributed Computing:Network programming, Servlets, CORBA, Enterprise JavaBeans
  16. A: Passing & Returning Objects:Aliasing, Making local copies, Cloning objects
  17. B: The Java Native Interface (JNI):Calling a native method, the JNIEnv argument
  18. Java Programming Guidelines:Design, Implementation
  19. Resources:Software, Books, My own list of books
  20. Index