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)
.
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
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. Anytime
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:
static int test(int testval) {
int result = 0;
if(testval > target)
result = -1;
else if(testval < target)
result = +1;
else
result = 0; // match
return result;
}
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:
static int test2(int testval) {
if(testval > target)
return -1;
if(testval < target)
return +1;
return 0; // match
}
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. Here’s
a simple example that generates random numbers until a particular condition is
met:
//: 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
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:
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:
//: 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++)
/* 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 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:
//: 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);
}
}
} ///:~
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 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 a new iteration.
This
program shows examples of
break
and
continue
within
for
and
while
loops:
//: 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. (Thus
printing happens only when the value of
i
is divisible by 9.) 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,
goto
jumps
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,
and in rare situations
goto
is the best way to structure control flow.
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:
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.
Here
is an example using
for
loops:
//: 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;
}
}
}
}
// 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.
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.) 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:
//: 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);
}
} ///:~
The
same rules hold true for
while:
- A
plain
continue
goes to the top of the innermost loop and continues.
- A
labeled
continue
goes to the label and re-enters the loop right after that label.
- A
break
“drops out of the bottom” of the loop.
- 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 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, for 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.
Here’s
an example that creates letters randomly and determines whether they’re
vowels or consonants:
//: 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.
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
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.
First,
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:
//: 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): " +
(char)('a' + below));
}
} ///:~
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.
The
second question has to do with 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 provides the answer:
//: RandomBounds.java
// Does Math.random() produce 0.0 and 1.0?
public class RandomBounds {
static void usage() {
System.err.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
System.out.println("Produced 1.0!");
}
else
usage();
}
} ///:~
To
run the program, you type a command line of either:
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 consider that there are 2
128
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).