|
|||||
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
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
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
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
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
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
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
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
//
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
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
}
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
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
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
}
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
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
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
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
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
//
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
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
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
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
}
}
///:~
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
{
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
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
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
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
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:
|
|||||