|
|||||
x
^= y;
x |=
y;
//
Casting:
//! boolean b =
(boolean)x;
char c =
(char)x;
byte
B = (byte)x;
int
i = (int)x;
long
l = (long)x;
float
f = (float)x;
double
d = (double)x;
}
void
intTest(int x, int y) {
//
Arithmetic operators:
x
= x * y;
x
= x / y;
x
= x % y;
x
= x + y;
x
= x - y;
x++;
x--;
x
= +y;
x
= -y;
//
Relational and logical:
f(x
> y);
f(x
>= y);
f(x
< y);
f(x
<= y);
f(x
== y);
f(x
!= y);
//!
f(!x);
//!
f(x && y);
//!
f(x || y);
//
Bitwise operators:
x
= ~y;
x
= x & y;
x
= x | y;
x
= x ^ y;
x
= x << 1;
x
= x >> 1;
x
= x >>> 1;
//
Compound assignment:
164
Thinking
in Java
x
+= y;
x
-= y;
x
*= y;
x
/= y;
x
%= y;
x
<<= 1;
x
>>= 1;
x
>>>= 1;
x
&= y;
x
^= y;
x
|= y;
//
Casting:
//!
boolean b = (boolean)x;
char
c = (char)x;
byte
B = (byte)x;
short
s = (short)x;
long
l = (long)x;
float
f = (float)x;
double
d = (double)x;
}
void
longTest(long x, long y) {
//
Arithmetic operators:
x
= x * y;
x
= x / y;
x
= x % y;
x
= x + y;
x
= x - y;
x++;
x--;
x
= +y;
x
= -y;
//
Relational and logical:
f(x
> y);
f(x
>= y);
f(x
< y);
f(x
<= y);
f(x
== y);
f(x
!= y);
//!
f(!x);
//!
f(x && y);
//!
f(x || y);
Chapter
3: Controlling Program Flow
165
//
Bitwise operators:
x
= ~y;
x
= x & y;
x
= x | y;
x
= x ^ y;
x
= x << 1;
x
= x >> 1;
x
= x >>> 1;
//
Compound assignment:
x
+= y;
x
-= y;
x
*= y;
x
/= y;
x
%= y;
x
<<= 1;
x
>>= 1;
x
>>>= 1;
x
&= y;
x
^= y;
x
|= y;
//
Casting:
//!
boolean b = (boolean)x;
char
c = (char)x;
byte
B = (byte)x;
short
s = (short)x;
int
i = (int)x;
float
f = (float)x;
double
d = (double)x;
}
void
floatTest(float x, float y) {
//
Arithmetic operators:
x
= x * y;
x
= x / y;
x
= x % y;
x
= x + y;
x
= x - y;
x++;
x--;
x
= +y;
x
= -y;
//
Relational and logical:
166
Thinking
in Java
f(x
> y);
f(x
>= y);
f(x
< y);
f(x
<= y);
f(x
== y);
f(x
!= y);
//!
f(!x);
//!
f(x && y);
//!
f(x || y);
//
Bitwise operators:
//!
x = ~y;
//!
x = x & y;
//!
x = x | y;
//!
x = x ^ y;
//!
x = x << 1;
//!
x = x >> 1;
//!
x = x >>> 1;
//
Compound assignment:
x
+= y;
x
-= y;
x
*= y;
x
/= y;
x
%= y;
//!
x <<= 1;
//!
x >>= 1;
//!
x >>>= 1;
//!
x &= y;
//!
x ^= y;
//!
x |= y;
//
Casting:
//!
boolean b = (boolean)x;
char
c = (char)x;
byte
B = (byte)x;
short
s = (short)x;
int
i = (int)x;
long
l = (long)x;
double
d = (double)x;
}
void
doubleTest(double x, double y) {
//
Arithmetic operators:
x
= x * y;
Chapter
3: Controlling Program Flow
167
x
= x / y;
x
= x % y;
x
= x + y;
x
= x - y;
x++;
x--;
x
= +y;
x
= -y;
//
Relational and logical:
f(x
> y);
f(x
>= y);
f(x
< y);
f(x
<= y);
f(x
== y);
f(x
!= y);
//!
f(!x);
//!
f(x && y);
//!
f(x || y);
//
Bitwise operators:
//!
x = ~y;
//!
x = x & y;
//!
x = x | y;
//!
x = x ^ y;
//!
x = x << 1;
//!
x = x >> 1;
//!
x = x >>> 1;
//
Compound assignment:
x
+= y;
x
-= y;
x
*= y;
x
/= y;
x
%= y;
//!
x <<= 1;
//!
x >>= 1;
//!
x >>>= 1;
//!
x &= y;
//!
x ^= y;
//!
x |= y;
//
Casting:
//!
boolean b = (boolean)x;
char
c = (char)x;
168
Thinking
in Java
byte
B = (byte)x;
short
s = (short)x;
int
i = (int)x;
long
l = (long)x;
float
f = (float)x;
}
}
///:~
Note
that boolean
is
quite limited. You can
assign to it the values
true
and
false,
and you can test it
for truth or falsehood, but
you cannot add
booleans
or perform any other type of
operation on them.
In
char,
byte,
and short
you
can see the effect of
promotion with the
arithmetic
operators. Each arithmetic
operation on any of those
types
results
in an int
result,
which must be explicitly
cast back to the
original
type
(a narrowing conversion that
might lose information) to
assign back
to
that type. With int values,
however, you do not need to
cast, because
everything
is already an int.
Don't be lulled into
thinking everything is
safe,
though. If you multiply two
ints
that are big enough,
you'll overflow
the
result. The following
example demonstrates
this:
//:
c03:Overflow.java
//
Surprise! Java lets you overflow.
public
class Overflow {
public
static void main(String[] args) {
int
big = 0x7fffffff; // max int value
prt("big
= " + big);
int
bigger = big * 4;
prt("bigger
= " + bigger);
}
static
void prt(String s) {
System.out.println(s);
}
}
///:~
The
output of this is:
big
= 2147483647
bigger
= -4
Chapter
3: Controlling Program Flow
169
and
you get no errors or
warnings from the compiler,
and no exceptions at
run-time.
Java is good, but it's
not that
good.
Compound
assignments do not
require
casts for char,
byte,
or
short,
even
though they are performing
promotions that have the
same results
as
the direct arithmetic
operations. On the other
hand, the lack of the
cast
certainly
simplifies the code.
You
can see that, with
the exception of boolean,
any primitive type
can
be
cast to any other primitive
type. Again, you must be
aware of the effect
of
a narrowing conversion when
casting to a smaller type,
otherwise you
might
unknowingly lose information
during the cast.
Execution
control
Java
uses all of C's execution
control statements, so if you've
programmed
with
C or C++ then most of what
you see will be familiar.
Most procedural
programming
languages have some kind of
control statements, and
there
is
often overlap among
languages. In Java, the
keywords include if-else,
while,
do-while,
for,
and a selection statement
called switch.
Java
does
not, however, support the
much-maligned goto
(which
can still be
the
most expedient way to solve
certain types of problems).
You can still
do
a goto-like jump, but it is
much more constrained than a
typical goto.
true
and false
All
conditional statements use
the truth or falsehood of a
conditional
expression
to determine the execution
path. An example of a
conditional
expression
is A
== B.
This uses the conditional
operator ==
to
see if the
value
of A
is
equivalent to the value of
B.
The expression returns
true or
false.
Any of the relational
operators you've seen
earlier in this
chapter
can
be used to produce a conditional
statement. Note that Java
doesn't
allow
you to use a number as a
boolean,
even though it's allowed in
C
and
C++ (where truth is nonzero
and falsehood is zero). If
you want to use
a
non-boolean
in
a boolean
test,
such as if(a),
you must first convert
it
to
a boolean
value
using a conditional expression,
such as if(a
!= 0).
170
Thinking
in Java
if-else
The
if-else
statement
is probably the most basic
way to control
program
flow.
The else
is
optional, so you can use
if in
two forms:
if(Boolean-expression)
statement
or
if(Boolean-expression)
statement
else
statement
The
conditional must produce a
boolean
result.
The statement
means
either
a simple statement terminated by a
semicolon or a compound
statement,
which is a group of simple
statements enclosed in braces.
Any
time
the word "statement"
is used, it always implies
that the statement
can
be simple or compound.
As
an example of if-else,
here is a test(
) method
that will tell
you
whether
a guess is above, below, or
equivalent to a target
number:
//:
c03:IfElse.java
public
class IfElse {
static
int test(int testval, int target) {
int
result = 0;
if(testval
> target)
result
= +1;
else
if(testval < target)
result
= -1;
else
result
= 0; // Match
return
result;
}
public
static void main(String[] args) {
System.out.println(test(10,
5));
System.out.println(test(5,
10));
System.out.println(test(5,
5));
}
}
///:~
Chapter
3: Controlling Program Flow
171
It
is conventional to indent the
body of a control flow
statement so the
reader
might easily determine where
it begins and ends.
return
The
return
keyword
has two purposes: it
specifies what value a
method
will
return (if it doesn't have a
void
return
value) and it causes that
value
to
be returned immediately. The
test( )
method
above can be rewritten
to
take
advantage of this:
//:
c03:IfElse2.java
public
class IfElse2 {
static
int test(int testval, int target) {
int
result = 0;
if(testval
> target)
return
+1;
else
if(testval < target)
return
-1;
else
return
0; // Match
}
public
static void main(String[] args) {
System.out.println(test(10,
5));
System.out.println(test(5,
10));
System.out.println(test(5,
5));
}
}
///:~
There's
no need for else
because
the method will not
continue after
executing
a return.
Iteration
while,
do-while
and
for control
looping and are sometimes
classified as
iteration
statements. A statement
repeats
until the controlling
Boolean-
expression
evaluates
to false. The form for a
while loop
is
while(Boolean-expression)
statement
The
Boolean-expression
is
evaluated once at the
beginning of the loop
and
again before each further
iteration of the statement.
172
Thinking
in Java
Here's
a simple example that
generates random numbers
until a
particular
condition is met:
//:
c03:WhileTest.java
//
Demonstrates the while loop.
public
class WhileTest {
public
static void main(String[] args) {
double
r = 0;
while(r
< 0.99d) {
r
= Math.random();
System.out.println(r);
}
}
}
///:~
This
uses the static
method
random( )
in
the Math
library,
which
generates
a double
value
between 0 and 1. (It
includes 0, but not 1.)
The
conditional
expression for the while says
"keep doing this loop
until the
number
is 0.99 or greater." Each
time you run this
program you'll get a
different-sized
list of numbers.
do-while
The
form for do-while
is
do
statement
while(Boolean-expression);
The
sole difference between
while and
do-while
is
that the statement of
the
do-while
always
executes at least once, even
if the expression
evaluates
to false the first time. In
a while,
if the conditional is false
the
first
time the statement never
executes. In practice, do-while
is
less
common
than while.
for
A
for loop
performs initialization before
the first iteration. Then
it
performs
conditional testing and, at
the end of each iteration,
some form
of
"stepping." The form of the
for loop
is:
Chapter
3: Controlling Program Flow
173
for(initialization; Boolean-expression; step)
statement
Any
of the expressions initialization,
Boolean-expression
or
step
can
be
empty.
The expression is tested
before each iteration, and
as soon as it
evaluates
to false
execution
will continue at the line
following the for
statement.
At the end of each loop,
the step
executes.
for
loops
are usually used for
"counting" tasks:
//:
c03:ListCharacters.java
//
Demonstrates "for" loop by listing
//
all the ASCII characters.
public
class ListCharacters {
public
static void main(String[] args) {
for(
char c = 0; c < 128; c++)
if
(c != 26 ) // ANSI Clear screen
System.out.println(
"value:
" + (int)c +
"
character: " + c);
}
}
///:~
Note
that the variable c is
defined at the point where
it is used, inside
the
control
expression of the for
loop,
rather than at the beginning
of the
block
denoted by the open curly
brace. The scope of
c is
the expression
controlled
by the for.
Traditional
procedural languages like C
require that all variables
be
defined
at the beginning of a block so
when the compiler creates a
block it
can
allocate space for those
variables. In Java and C++
you can spread
your
variable declarations throughout
the block, defining them at
the
point
that you need them.
This allows a more natural
coding style and
makes
code easier to
understand.
You
can define multiple
variables within a for
statement,
but they must
be
of the same type:
for(int
i = 0, j = 1;
i
< 10 && j != 11;
i++,
j++)
174
Thinking
in Java
/*
body of for loop */;
The
int definition
in the for
statement
covers both i
and
j.
The ability to
define
variables in the control
expression is limited to the
for loop.
You
cannot
use this approach with
any of the other selection
or iteration
statements.
The
comma operator
Earlier
in this chapter I stated
that the comma operator
(not
the comma
separator,
which is used to separate
definitions and function
arguments)
has
only one use in Java: in
the control expression of a
for loop.
In both
the
initialization and step
portions of the control
expression you can
have
a
number of statements separated by
commas, and those statements
will
be
evaluated sequentially. The
previous bit of code uses
this ability. Here's
another
example:
//:
c03:CommaOperator.java
public
class CommaOperator {
public
static void main(String[] args) {
for(int
i = 1, j = i + 10; i < 5;
i++,
j = i * 2) {
System.out.println("i=
" + i + " j= " + j);
}
}
}
///:~
Here's
the output:
i=
1
j=
11
i=
2
j=
4
i=
3
j=
6
i=
4
j=
8
You
can see that in both
the initialization and step
portions the
statements
are evaluated in sequential
order. Also, the
initialization
portion
can have any number of
definitions of
one type.
break
and continue
Inside
the body of any of the
iteration statements you can
also control the
flow
of the loop by using
break and
continue.
break quits
the loop
Chapter
3: Controlling Program Flow
175
without
executing the rest of the
statements in the loop.
continue
stops
the
execution of the current
iteration and goes back to
the beginning of
the
loop to begin the next
iteration.
This
program shows examples of
break and
continue
within
for and
while
loops:
//:
c03:BreakAndContinue.java
//
Demonstrates break and continue keywords.
public
class BreakAndContinue {
public
static void main(String[] args) {
for(int
i = 0; i < 100; i++) {
if(i
== 74) break; // Out of for loop
if(i
% 9 != 0) continue; // Next iteration
System.out.println(i);
}
int
i = 0;
//
An "infinite loop":
while(true)
{
i++;
int
j = i * 27;
if(j
== 1269) break; // Out of loop
if(i
% 10 != 0) continue; // Top of loop
System.out.println(i);
}
}
}
///:~
In
the for
loop
the value of i
never
gets to 100 because the
break
statement
breaks out of the loop
when i
is
74. Normally, you'd use
a
break
like
this only if you didn't
know when the terminating
condition
was
going to occur. The
continue
statement
causes execution to go
back
to
the top of the iteration
loop (thus incrementing
i)
whenever i
is
not
evenly
divisible by 9. When it is,
the value is printed.
The
second portion shows an
"infinite loop" that would,
in theory,
continue
forever. However, inside the
loop there is a break
statement
that
will break out of the
loop. In addition, you'll
see that the continue
moves
back to the top of the
loop without completing the
remainder.
176
Thinking
in Java
(Thus
printing happens in the
second loop only when
the value of i
is
divisible
by 10.) The output
is:
0
9
18
27
36
45
54
63
72
10
20
30
40
The
value 0 is printed because 0 % 9
produces 0.
A
second form of the infinite
loop is for(;;).
The compiler treats
both
while(true)
and
for(;;)
in
the same way so whichever
one you use is a
matter
of programming taste.
The
infamous "goto"
The
goto
keyword
has been present in
programming languages from
the
beginning.
Indeed, goto
was
the genesis of program
control in assembly
language:
"if condition A, then jump
here, otherwise jump there."
If you
read
the assembly code that is
ultimately generated by virtually
any
compiler,
you'll see that program
control contains many jumps.
However,
a
goto
is
a jump at the source-code
level, and that's what
brought it into
disrepute.
If a program will always
jump from one point to
another, isn't
there
some way to reorganize the
code so the flow of control
is not so
jumpy?
goto
fell
into true disfavor with
the publication of the
famous
"Goto
considered harmful" paper by
Edsger Dijkstra, and since
then goto-
bashing
has been a popular sport,
with advocates of the
cast-out keyword
scurrying
for cover.
As
is typical in situations like
this, the middle ground is
the most fruitful.
The
problem is not the use of
goto,
but the overuse of goto--in
rare
situations
goto
is
actually the best way to
structure control
flow.
Chapter
3: Controlling Program Flow
177
Although
goto
is
a reserved word in Java, it is
not used in the
language;
Java
has no goto.
However, it does have
something that looks a bit
like a
jump
tied in with the break and
continue
keywords.
It's not a jump
but
rather
a way to break out of an
iteration statement. The
reason it's often
thrown
in with discussions of goto
is
because it uses the
same
mechanism:
a label.
A
label is an identifier followed by a
colon, like this:
label1:
The
only
place
a label is useful in Java is
right before an
iteration
statement.
And that means right
before--it
does no good to put any
other
statement
between the label and
the iteration. And the
sole reason to put
a
label before an iteration is if
you're going to nest another
iteration or a
switch
inside it. That's because
the break
and
continue
keywords
will
normally
interrupt only the current
loop, but when used
with a label
they'll
interrupt the loops up to
where the label
exists:
label1:
outer-iteration
{
inner-iteration
{
//...
break;
// 1
//...
continue;
// 2
//...
continue
label1; // 3
//...
break
label1; // 4
}
}
In
case 1, the break
breaks
out of the inner iteration
and you end up in
the
outer iteration. In case 2,
the continue
moves
back to the beginning
of
the inner iteration. But in
case 3, the continue
label1 breaks
out of
the
inner iteration and
the
outer iteration, all the
way back to label1.
Then
it does in fact continue the
iteration, but starting at
the outer
iteration.
In case 4, the break
label1 also
breaks all the way
out to
label1,
but it does not re-enter
the iteration. It actually
does break out of
both
iterations.
178
Thinking
in Java
Here
is an example using for
loops:
//:
c03:LabeledFor.java
//
Java's "labeled for" loop.
public
class LabeledFor {
public
static void main(String[] args) {
int
i = 0;
outer:
// Can't have statements here
for(;
true ;) { // infinite loop
inner:
// Can't have statements here
for(;
i < 10; i++) {
prt("i
= " + i);
if(i
== 2) {
prt("continue");
continue;
}
if(i
== 3) {
prt("break");
i++;
// Otherwise i never
//
gets incremented.
break;
}
if(i
== 7) {
prt("continue
outer");
i++;
// Otherwise i never
//
gets incremented.
continue
outer;
}
if(i
== 8) {
prt("break
outer");
break
outer;
}
for(int
k = 0; k < 5; k++) {
if(k
== 3) {
prt("continue
inner");
continue
inner;
}
}
}
}
Chapter
3: Controlling Program Flow
179
//
Can't break or continue
//
to labels here
}
static
void prt(String s) {
System.out.println(s);
}
}
///:~
This
uses the prt(
) method
that has been defined in
the other examples.
Note
that break
breaks
out of the for
loop,
and that the
increment-
expression
doesn't occur until the
end of the pass through
the for
loop.
Since
break skips
the increment expression,
the increment is
performed
directly
in the case of i
== 3.
The continue
outer statement in
the case
of
i == 7
also
goes to the top of the
loop and also skips
the increment, so
it
too is incremented
directly.
Here
is the output:
i=0
continue
inner
i=1
continue
inner
i=2
continue
i=3
break
i=4
continue
inner
i=5
continue
inner
i=6
continue
inner
i=7
continue
outer
i=8
break
outer
If
not for the break
outer statement,
there would be no way to get
out of
the
outer loop from within an
inner loop, since break by
itself can break
out
of only the innermost loop.
(The same is true for
continue.)
180
Thinking
in Java
Of
course, in the cases where
breaking out of a loop will
also exit the
method,
you can simply use a
return.
Here
is a demonstration of labeled break and
continue
statements
with
while
loops:
//:
c03:LabeledWhile.java
//
Java's "labeled while" loop.
public
class LabeledWhile {
public
static void main(String[] args) {
int
i = 0;
outer:
while(true)
{
prt("Outer
while loop");
while(true)
{
i++;
prt("i
= " + i);
if(i
== 1) {
prt("continue");
continue;
}
if(i
== 3) {
prt("continue
outer");
continue
outer;
}
if(i
== 5) {
prt("break");
break;
}
if(i
== 7) {
prt("break
outer");
break
outer;
}
}
}
}
static
void prt(String s) {
System.out.println(s);
}
}
///:~
Chapter
3: Controlling Program Flow
181
The
same rules hold true
for while:
1.
A
plain continue
goes
to the top of the innermost
loop and
continues.
2.
A
labeled continue
goes
to the label and re-enters
the loop right
after
that label.
3.
A
break "drops
out of the bottom" of the
loop.
4.
A
labeled break
drops
out of the bottom of the
end of the loop
denoted
by the label.
The
output of this method makes
it clear:
Outer
while loop
i=1
continue
i=2
i=3
continue
outer
Outer
while loop
i=4
i=5
break
Outer
while loop
i=6
i=7
break
outer
It's
important to remember that
the only
reason
to use labels in Java
is
when
you have nested loops
and you want to break or
continue
through
more
than one nested
level.
In
Dijkstra's "goto considered
harmful" paper, what he
specifically
objected
to was the labels, not
the goto. He observed that
the number of
bugs
seems to increase with the
number of labels in a program.
Labels
and
gotos make programs
difficult to analyze statically,
since it introduces
cycles
in the program execution
graph. Note that Java
labels don't suffer
from
this problem, since they
are constrained in their
placement and can't
be
used to transfer control in an ad
hoc manner. It's also
interesting to
182
Thinking
in Java
note
that this is a case where a
language feature is made
more useful by
restricting
the power of the
statement.
switch
The
switch
is
sometimes classified as a selection
statement. The
switch
statement
selects from among pieces of
code based on the value of
an
integral
expression. Its form
is:
switch(integral-selector) {
case
integral-value1
:
statement;
break;
case
integral-value2
:
statement;
break;
case
integral-value3
:
statement;
break;
case
integral-value4
:
statement;
break;
case
integral-value5
:
statement;
break;
//
...
default:
statement;
}
Integral-selector
is
an expression that produces an
integral value. The
switch
compares
the result of integral-selector
to
each integral-value.
If
it
finds a match, the
corresponding statement
(simple
or compound)
executes.
If no match occurs, the
default
statement
executes.
You
will notice in the above
definition that each
case
ends
with a break,
which
causes execution to jump to
the end of the switch
body.
This is the
conventional
way to build a switch
statement,
but the break
is
optional.
If
it is missing, the code for
the following case
statements execute until
a
break
is
encountered. Although you
don't usually want this
kind of
behavior,
it can be useful to an experienced
programmer. Note the
last
statement,
following the default,
doesn't have a break
because
the
execution
just falls through to where
the break
would
have taken it
anyway.
You could put a break at
the end of the default
statement
with
no
harm if you considered it
important for style's
sake.
The
switch
statement
is a clean way to implement
multi-way selection
(i.e.,
selecting from among a
number of different execution
paths), but it
requires
a selector that evaluates to an
integral value such as
int or
char.
If
you want to use, for
example, a string or a floating-point
number as a
selector,
it won't work in a switch
statement.
For non-integral types,
you
must
use a series of if
statements.
Chapter
3: Controlling Program Flow
183
Here's
an example that creates
letters randomly and
determines whether
they're
vowels or consonants:
//:
c03:VowelsAndConsonants.java
//
Demonstrates the switch statement.
public
class VowelsAndConsonants {
public
static void main(String[] args) {
for(int
i = 0; i < 100; i++) {
char
c = (char)(Math.random() * 26 + 'a');
System.out.print(c
+ ": ");
switch(c)
{
case
'a':
case
'e':
case
'i':
case
'o':
case
'u':
System.out.println("vowel");
break;
case
'y':
case
'w':
System.out.println(
"Sometimes
a vowel");
break;
default:
System.out.println("consonant");
}
}
}
}
///:~
Since
Math.random( )
generates
a value between 0 and 1, you
need
only
multiply it by the upper
bound of the range of
numbers you want to
produce
(26 for the letters in
the alphabet) and add an
offset to establish
the
lower bound.
Although
it appears you're switching on a
character here, the
switch
statement
is actually using the
integral value of the
character. The
singly-
quoted
characters in the case
statements
also produce integral
values
that
are used for
comparison.
184
Thinking
in Java
Notice
how the cases
can be "stacked" on top of
each other to provide
multiple
matches for a particular
piece of code. You should
also be aware
that
it's essential to put the
break statement
at the end of a
particular
case,
otherwise control will
simply drop through and
continue processing
on
the next case.
Calculation
details
The
statement:
char
c = (char)(Math.random() * 26 + 'a');
deserves
a closer look. Math.random(
) produces
a double,
so the
value
26 is converted to a double
to
perform the multiplication,
which
also
produces a double.
This means that `a'
must
be converted to a
double
to
perform the addition. The
double
result
is turned back into a
char
with
a cast.
What
does the cast to char do?
That is, if you have
the value 29.7 and
you
cast
it to a char,
is the resulting value 30 or
29? The answer to this
can be
seen
in this example:
//:
c03:CastingNumbers.java
//
What happens when you cast a float
//
or double to an integral value?
public
class CastingNumbers {
public
static void main(String[] args) {
double
above
= 0.7,
below
= 0.4;
System.out.println("above:
" + above);
System.out.println("below:
" + below);
System.out.println(
"(int)above:
" + (int)above);
System.out.println(
"(int)below:
" + (int)below);
System.out.println(
"(char)('a'
+ above): " +
(char)('a'
+ above));
System.out.println(
"(char)('a'
+ below): " +
Chapter
3: Controlling Program Flow
185
(char)('a'
+ below));
}
}
///:~
The
output is:
above:
0.7
below:
0.4
(int)above:
0
(int)below:
0
(char)('a'
+ above): a
(char)('a'
+ below): a
So
the answer is that casting
from a float
or
double
to
an integral value
always
truncates.
A
second question concerns
Math.random(
).
Does it produce a
value
from
zero to one, inclusive or
exclusive of the value `1'?
In math lingo, is it
(0,1),
or [0,1], or (0,1] or [0,1)?
(The square bracket means
"includes"
whereas
the parenthesis means
"doesn't include.") Again, a
test program
might
provide the answer:
//:
c03:RandomBounds.java
//
Does Math.random() produce 0.0 and 1.0?
public
class RandomBounds {
static
void usage() {
System.out.println("Usage:
\n\t" +
"RandomBounds
lower\n\t" +
"RandomBounds
upper");
System.exit(1);
}
public
static void main(String[] args) {
if(args.length
!= 1) usage();
if(args[0].equals("lower"))
{
while(Math.random()
!= 0.0)
;
// Keep trying
System.out.println("Produced
0.0!");
}
else
if(args[0].equals("upper")) {
while(Math.random()
!= 1.0)
;
// Keep trying
186
Thinking
in Java
System.out.println("Produced
1.0!");
}
else
usage();
}
}
///:~
To
run the program, you
type a command line of
either:
java
RandomBounds lower
or
java
RandomBounds upper
In
both cases you are
forced to break out of the
program manually, so it
would
appear
that
Math.random( )
never
produces either 0.0 or
1.0.
But
this is where such an
experiment can be deceiving. If
you consider2
that
there are about 262 different double fractions
between 0 and 1, the
likelihood
of reaching any one value
experimentally might exceed
the
lifetime
of one computer, or even one
experimenter. It turns out
that 0.0
is
included
in the output of Math.random(
).
Or, in math lingo, it
is
[0,1).
Summary
This
chapter concludes the study
of fundamental features that
appear in
most
programming languages: calculation,
operator precedence,
type
2
Chuck Allison
writes: The total number of numbers in a
floating-point number system is
2(M-m+1)b^(p-1)
+ 1
where
b is
the base (usually 2),
p is
the precision (digits in the mantissa),
M is
the largest
exponent,
and m
is
the smallest exponent. IEEE
754 uses:
M
= 1023, m = -1022, p = 53, b =
2
so
the total number of numbers is
2(1023+1022+1)2^52
=
2((2^10-1) + (2^10-1))2^52
=
(2^10-1)2^54
=
2^64 - 2^54
Half
of these numbers (corresponding to
exponents in the range [-1022, 0]) are
less than 1
in
magnitude (both positive and
negative), so 1/4 of that
expression, or 2^62 - 2^52 + 1
(approximately
2^62) is in the range [0,1). See my
paper at
http://www.freshsources.com/1995006a.htm
(last of text).
Chapter
3: Controlling Program Flow
187
casting,
and selection and iteration.
Now you're ready to begin
taking
steps
that move you closer to
the world of object-oriented
programming.
The
next chapter will cover
the important issues of
initialization and
cleanup
of objects, followed in the
subsequent chapter by the
essential
concept
of implementation hiding.
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.
There
are two expressions in the
section labeled
"precedence"
early
in this chapter. Put these
expressions into a program
and
demonstrate
that they produce different
results.
2.
Put
the methods ternary( ) and
alternative( ) into a
working
program.
3.
From
the sections labeled
"if-else" and "return", put
the methods
test(
) and test2( ) into a
working program.
4.
Write
a program that prints values
from one to 100.
5.
Modify
Exercise 4 so that the
program exits by using the
break
keyword
at value 47. Try using
return instead.
6.
Write
a function that takes two
String arguments, and uses
all the
Boolean
comparisons to compare the
two Strings and print
the
results.
For the == and !=,
also perform the equals( )
test. In
main(
), call your function with
some different String
objects.
7.
Write
a program that generates 25
random int values. For
each
value,
use an if-then-else statement to
classify it as greater
than,
less
than or equal to a second
randomly-generated value.
8.
Modify
Exercise 7 so that your code
is surrounded by an "infinite"
while
loop. It will then run
until you interrupt it from
the keyboard
(typically
by pressing Control-C).
9.
Write
a program that uses two
nested for loops and
the modulus
operator
(%) to detect and print
prime numbers (integral
numbers
188
Thinking
in Java
that
are not evenly divisible by
any other numbers except
for
themselves
and 1).
10.
Create
a switch statement that
prints a message for each
case, and
put
the switch inside a for
loop that tries each
case. Put a break
after
each case and test
it, then remove the
breaks and see
what
happens.
Chapter
3: Controlling Program Flow
189
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.
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
191
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.
class
Rock {
Rock()
{ // This is the constructor
System.out.println("Creating
Rock");
}
}
public
class SimpleConstructor {
public
static void main(String[] args) {
for(int
i = 0; i < 10; i++)
new
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.
192
Thinking
in Java
Like
any method, the constructor
can have arguments to allow
you to
specify
how
an
object is created. The above
example can easily be
changed
so
the constructor takes an
argument:
//:
c04:SimpleConstructor2.java
//
Constructors can have arguments.
class
Rock2 {
Rock2(int
i) {
System.out.println(
"Creating
Rock number " + i);
}
}
public
class SimpleConstructor2 {
public
static void main(String[] args) {
for(int
i = 0; i < 10; i++)
new
Rock2(i);
}
}
///:~
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
definition. In Java, definition
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
Chapter
4: Initialization &
Cleanup
193
option.
If there was a return value,
and if you could select
your own, the
compiler
would somehow need to know
what to do with that return
value.
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,
also called the no-arg
constructor),
and one that takes a
String
as
an argument, which is
the
194
Thinking
in Java
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
java.util.*;
class
Tree {
int
height;
Tree()
{
prt("Planting
a seedling");
height
= 0;
}
Tree(int
i) {
prt("Creating
new Tree that is "
+
i + " feet tall");
height
= i;
}
void
info() {
prt("Tree
is " + height
+
" feet tall");
}
void
info(String s) {
prt(s
+ ": Tree is "
+
height + " feet tall");
}
static
void prt(String s) {
System.out.println(s);
}
}
public
class Overloading {
public
static void main(String[] args) {
for(int
i = 0; i < 5; i++) {
Chapter
4: Initialization &
Cleanup
195
Tree
t = new Tree(i);
t.info();
t.info("overloaded
method");
}
//
Overloaded constructor:
new
Tree();
}
}
///:~
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
are
two constructors, one that
takes no arguments (we call
constructors
that
take no arguments default
constructors1) and one that
takes the
existing
height.
You
might also want to call
the info(
) method
in more than one way.
For
example,
with a String
argument
if you have an extra message
you want
printed,
and without 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.
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.)
1
In some of the
Java literature from Sun
they instead refer to these
with the clumsy but
descriptive
name "no-arg constructors." The
term "default constructor"
has been in use
for
many
years and so I will use
that.
196
Thinking
in Java
//:
c04:OverloadingOrder.java
//
Overloading based on the order of
//
the arguments.
public
class OverloadingOrder {
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");
}
}
///:~
The
two print(
) methods
have identical arguments,
but the order is
different,
and that's what makes
them distinct.
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.
public
class PrimitiveOverloading {
//
boolean can't be automatically converted
static
void prt(String s) {
System.out.println(s);
}
void
f1(char x) { prt("f1(char)"); }
Chapter
4: Initialization &
Cleanup
197
void
f1(byte
x) { prt("f1(byte)"); }
void
f1(short
x) { prt("f1(short)"); }
void
f1(int
x) { prt("f1(int)"); }
void
f1(long
x) { prt("f1(long)"); }
void
f1(float
x) { prt("f1(float)"); }
void
f1(double
x) { prt("f1(double)"); }
void
f2(byte
x) { prt("f2(byte)"); }
void
f2(short
x) { prt("f2(short)"); }
void
f2(int
x) { prt("f2(int)"); }
void
f2(long
x) { prt("f2(long)"); }
void
f2(float
x) { prt("f2(float)"); }
void
f2(double
x) { prt("f2(double)"); }
void
f3(short
x) { prt("f3(short)"); }
void
f3(int
x) { prt("f3(int)"); }
void
f3(long
x) { prt("f3(long)"); }
void
f3(float
x) { prt("f3(float)"); }
void
f3(double
x) { prt("f3(double)"); }
void
f4(int
x) { prt("f4(int)"); }
void
f4(long
x) { prt("f4(long)"); }
void
f4(float
x) { prt("f4(float)"); }
void
f4(double
x) { prt("f4(double)"); }
void
f5(long x) { prt("f5(long)"); }
void
f5(float x) { prt("f5(float)"); }
void
f5(double x) { prt("f5(double)"); }
void
f6(float x) { prt("f6(float)"); }
void
f6(double x) { prt("f6(double)"); }
void
f7(double x) { prt("f7(double)"); }
void
testConstVal() {
prt("Testing
with 5");
f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);
}
void
testChar() {
char
x = 'x';
prt("char
argument:");
198
Thinking
in Java
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void
testByte() {
byte
x = 0;
prt("byte
argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void
testShort() {
short
x = 0;
prt("short
argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void
testInt() {
int
x = 0;
prt("int
argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void
testLong() {
long
x = 0;
prt("long
argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void
testFloat() {
float
x = 0;
prt("float
argument:");
f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
}
void
testDouble() {
double
x = 0;
prt("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();
Chapter
4: Initialization &
Cleanup
199
p.testFloat();
p.testDouble();
}
}
///:~
If
you view the output of
this program, 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 above program gives
the
answer:
//:
c04:Demotion.java
//
Demotion of primitives and overloading.
public
class Demotion {
static
void prt(String s) {
System.out.println(s);
}
void
f1(char
x) { prt("f1(char)"); }
void
f1(byte
x) { prt("f1(byte)"); }
void
f1(short
x) { prt("f1(short)"); }
void
f1(int
x) { prt("f1(int)"); }
void
f1(long
x) { prt("f1(long)"); }
void
f1(float
x) { prt("f1(float)"); }
void
f1(double
x) { prt("f1(double)"); }
void
f2(char
x) { prt("f2(char)"); }
void
f2(byte
x) { prt("f2(byte)"); }
void
f2(short
x) { prt("f2(short)"); }
void
f2(int
x) { prt("f2(int)"); }
void
f2(long
x) { prt("f2(long)"); }
void
f2(float
x) { prt("f2(float)"); }
void
f3(char x) { prt("f3(char)"); }
void
f3(byte x) { prt("f3(byte)"); }
200
Thinking
in Java
void
f3(short x) { prt("f3(short)"); }
void
f3(int x) { prt("f3(int)"); }
void
f3(long x) { prt("f3(long)"); }
void
f4(char
x) { prt("f4(char)"); }
void
f4(byte
x) { prt("f4(byte)"); }
void
f4(short
x) { prt("f4(short)"); }
void
f4(int
x) { prt("f4(int)"); }
void
f5(char x) { prt("f5(char)"); }
void
f5(byte x) { prt("f5(byte)"); }
void
f5(short x) { prt("f5(short)"); }
void
f6(char x) { prt("f6(char)"); }
void
f6(byte x) { prt("f6(byte)"); }
void
f7(char x) { prt("f7(char)"); }
void
testDouble() {
double
x = 0;
prt("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();
}
}
///:~
Here,
the methods take narrower
primitive values. If your
argument is
wider
then you must cast
to
the necessary type using
the type name in
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.
Chapter
4: Initialization &
Cleanup
201
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 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.
Default
constructors
As
mentioned previously, a default
constructor (a.k.a. a
"no-arg"
constructor)
is one without arguments,
used to create a "vanilla
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!
}
}
///:~
202
Thinking
in Java
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
Bush {
Bush(int
i) {}
Bush(double
d) {}
}
Now
if you say:
new
Bush();
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."
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
above become something
like:
Chapter
4: Initialization &
Cleanup
203
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.
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.
public
class Leaf {
int
i = 0;
Leaf
increment() {
i++;
return
this;
}
void
print() {
System.out.println("i
= " + i);
}
public
static void main(String[] args) {
204
Thinking
in Java
Leaf
x = new Leaf();
x.increment().increment().increment().print();
}
}
///:~
Because
increment( )
returns
the reference to the current
object via the
this
keyword,
multiple operations can
easily be performed on the
same
object.
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 do this 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."
public
class Flower {
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!
Chapter
4: Initialization &
Cleanup
205
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();
}
}
///:~
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 by saying this.s
to
refer 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.
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
206
Thinking
in Java
static
methods2 (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). Except 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."
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 a
very unusual
case.
Suppose your object
allocates "special" memory
without using new.
The
garbage collector knows only
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
2
The one
case in which this is possible occurs if
you pass a reference to an
object into the
static
method.
Then, via the reference (which is now
effectively this),
you can call non-
static
methods
and access non-static
fields.
But typically if you want to
do something like
this
you'll just make an ordinary,
non-static
method.
Chapter
4: Initialization &
Cleanup
207
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:
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 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,
the
image
will first be removed from
the screen, but if it isn't,
the image will
remain.
So a second point to remember
is:
Your
objects might not get
garbage-collected.
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.
What
is finalize( ) for?
You
might believe at this point
that you should not
use finalize(
) as
a
general-purpose
cleanup method. What good is
it?
208
Thinking
in Java
A
third point to remember
is:
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 discussed in Appendix
B.) 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.
You're correct; it is not
the appropriate place
for
normal
cleanup to occur. So where
should normal cleanup be
performed?
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
Chapter
4: Initialization &
Cleanup
209
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.
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
since 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.
One
of the things finalize(
) can
be useful for is observing
the process of
garbage
collection. The following
example shows you what's
going on and
summarizes
the previous descriptions of
garbage collection:
//:
c04:Garbage.java
//
Demonstration of the garbage
//
collector and finalization
class
Chair {
static
boolean gcrun = false;
static
boolean f = false;
static
int created = 0;
static
int finalized = 0;
int
i;
Chair()
{
i
= ++created;
if(created
== 47)
System.out.println("Created
47");
}
210
Thinking
in Java
public
void finalize() {
if(!gcrun)
{
//
The first time finalize() is called:
gcrun
= true;
System.out.println(
"Beginning
to finalize after " +
created
+ " Chairs have been created");
}
if(i
== 47) {
System.out.println(
"Finalizing
Chair #47, " +
"Setting
flag to stop Chair creation");
f
= true;
}
finalized++;
if(finalized
>= created)
System.out.println(
"All
" + finalized + " finalized");
}
}
public
class Garbage {
public
static void main(String[] args) {
//
As long as the flag hasn't been set,
//
make Chairs and Strings:
while(!Chair.f)
{
new
Chair();
new
String("To take up space");
}
System.out.println(
"After
all Chairs have been created:\n" +
"total
created = " + Chair.created +
",
total finalized = " + Chair.finalized);
//
Optional arguments force garbage
//
collection & finalization:
if(args.length
> 0) {
if(args[0].equals("gc")
||
args[0].equals("all"))
{
System.out.println("gc():");
System.gc();
}
Chapter
4: Initialization &
Cleanup
211
if(args[0].equals("finalize")
||
args[0].equals("all"))
{
System.out.println("runFinalization():");
System.runFinalization();
}
}
System.out.println("bye!");
}
}
///:~
The
above program creates many
Chair objects,
and at some point
after
the
garbage collector begins
running, the program stops
creating Chairs.
Since
the garbage collector can
run at any time, you
don't know exactly
when
it will start up, so there's
a flag called gcrun
to
indicate whether the
garbage
collector has started
running yet. A second flag
f is
a way for
Chair
to
tell the main(
) loop
that it should stop making
objects. Both of
these
flags are set within
finalize(
),
which is called during
garbage
collection.
Two
other static
variables,
created
and
finalized,
keep track of the
number
of Chairs
created versus the number
that get finalized by
the
garbage
collector. Finally, each
Chair has
its own (non-static)
int i so
it
can
keep track of what number it
is. When Chair
number
47 is finalized,
the
flag is set to true
to
bring the process of
Chair creation
to a stop.
All
this happens in main(
),
in the loop
while(!Chair.f)
{
new
Chair();
new
String("To take up space");
}
You
might wonder how this
loop could ever finish,
since there's nothing
inside
the loop that changes
the value of Chair.f.
However, the
finalize(
) process
will, eventually, when it
finalizes number 47.
The
creation of a String
object
during each iteration is
simply extra
storage
being allocated to encourage
the garbage collector to
kick in,
which
it will do when it starts to
get nervous about the
amount of memory
available.
212
Thinking
in Java
When
you run the program,
you provide a command-line
argument of
"gc,"
"finalize," or "all." The
"gc" argument will call
the System.gc(
)
method
(to force execution of the
garbage collector). Using
the "finalize"
argument
calls System.runFinalization(
) which--in
theory--will
cause
any unfinalized objects to be
finalized. And "all" causes
both
methods
to be called.
The
behavior of this program and
the version in the first
edition of this
book
shows that the whole
issue of garbage collection
and finalization has
been
evolving, with much of the
evolution happening behind
closed doors.
In
fact, by the time you
read this, the behavior of
the program may
have
changed
once again.
If
System.gc(
) is
called, then finalization
happens to all the objects.
This
was
not necessarily the case
with previous implementations of
the JDK,
although
the documentation claimed
otherwise. In addition, you'll
see
that
it doesn't seem to make any
difference whether
System.runFinalization(
) is
called.
However,
you will see that
only if System.gc(
) is
called after all
the
objects
are created and discarded
will all the finalizers be
called. If you do
not
call System.gc(
),
then only some of the
objects will be finalized.
In
Java
1.1, a method System.runFinalizersOnExit(
) was
introduced
that
caused programs to run all
the finalizers as they
exited, but the
design
turned out to be buggy and
the method was deprecated.
This is yet
another
clue that the Java
designers were thrashing
about trying to solve
the
garbage collection and
finalization problem. We can
only hope that
things
have been worked out in
Java 2.
The
preceding program shows that
the promise that finalizers
will always
be
run holds true, but
only if you explicitly force
it to happen yourself. If
you
don't cause System.gc(
) to
be called, you'll get an
output like this:
Created
47
Beginning
to finalize
after
3486 Chairs have been
created
Finalizing
Chair #47,
Setting
flag to stop Chair
creation
After
all Chairs have
been
created:
total
created = 3881,
total
finalized = 2684
Chapter
4: Initialization &
Cleanup
213
bye!
Thus,
not all finalizers get
called by the time the
program completes. If
System.gc(
) is
called, it will finalize and
destroy all the objects
that are
no
longer in use up to that
point.
Remember
that neither garbage
collection nor finalization is
guaranteed.
If
the Java Virtual Machine
(JVM) isn't close to running
out of memory,
then
it will (wisely) not waste
time recovering memory
through garbage
collection.
The
death condition
In
general, you can't rely on
finalize(
) being
called, and you must
create
separate
"cleanup" functions 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(
) which
does not rely on it being
called every time. This is
the
verification
of the death
condition3 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
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:DeathCondition.java
//
Using finalize() to detect an object that
//
hasn't been properly cleaned up.
class
Book {
3
A term coined
by Bill Venners (www.artima.com) during a seminar
that he and I were
giving
together.
214
Thinking
in Java
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 DeathCondition {
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();
}
}
///:~
The
death 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 death
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).
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
expensive.
Chapter
4: Initialization &
Cleanup
215
Table of Contents:
|
|||||