Chapter 10. Control Flow

 

“Would you tell me, please, which way I ought to go from here?” “That depends a good deal on where you want to get to.”

 
 --Lewis Carroll, Alice in Wonderland

A program consisting only of a list of consecutive statements is immediately useful because the statements are executed in the order in which they're written. But the ability to control the order in which statements are executed—that is, to test conditions and execute different statements based on the results of the tests—adds enormous value to our programming toolkit. This chapter covers almost all the control flow statements that direct the order of execution. Exceptions and assertions are covered separately in Chapter 12.

Statements and Blocks

The two basic statements are expression statements and declaration statements, of which you've seen a plethora. Expression statements, such as i++ or method invocations, are expressions that have a semicolon at the end. The semicolon terminates the statement.[1] In fact, a semicolon by itself is a statement that does nothing—the empty statement. Not all expressions can become statements, since it would be almost always meaningless to have, for example, an expression such as x<= y stand alone as a statement. Only the following types of expressions can be made into statements by adding a terminating semicolon:

  • Assignment expressions—those that contain = or one of the op= operators

  • Prefix or postfix forms of ++ and --

  • Method calls (whether or not they return a value)

  • Object creation expressions—those that use new to create an object

Declaration statements (formally called local variable declaration statements) declare a variable and initialize it to a value, as discussed in Section 7.3.1 on page 170. They can appear anywhere inside a block, not just at the beginning. Local variables exist only as long as the block containing their declaration is executing. Local variables must be initialized before use, either by initialization when declared or by assignment. If any local variable is used before it is initialized, the code will not compile.

Local class declaration statements declare a local inner class that can be used within the block in which it was declared. Local classes were discussed in detail on page 142.

In addition to the expression statements listed, several other kinds of statements, such as if and for statements, affect flow of control through the program. This chapter covers each type of statement in detail.

Curly braces, { and }, group zero or more statements into a block. A block can be used where any single statement is allowed because a block is a statement, albeit a compound one.

ifelse

The most basic form of conditional control flow is the if statement, which chooses whether to execute statements that follow it. Its syntax is:

if (expression)
    statement1
else
    statement2

First, the expression—which must be of type boolean or Boolean—is evaluated. If its value is true, then statement1 is executed; otherwise, if there is an else clause, statement2 is executed. The else clause is optional.

You can build a series of tests by joining another if to the else clause of a previous if. Here is a method that maps a string—expected to be one of a particular set of words—into an action to be performed with a value:

public void setProperty(String keyword, double value)
    throws UnknownProperty
{
    if (keyword.equals("charm"))
        charm(value);
    else if (keyword.equals("strange"))
        strange(value);
    else
        throw new UnknownProperty(keyword);
}

What if there is more than one preceding if without an else? For example:

public double sumPositive(double[] values) {
    double sum = 0.0;

    if (values.length > 1)
        for (int i = 0; i < values.length; i++)
            if (values[i] > 0)
                sum += values[i];
    else    // oops!
        sum = values[0];
    return sum;
}

The else clause looks as if it is bound to the array length check, but that is a mirage of indentation, and indentation is ignored. Instead, an else clause is bound to the most recent if that does not have one. Thus, the previous block of code is equivalent to

public double sumPositive(double[] values) {
    double sum = 0.0;

    if (values.length > 1)
        for (int i = 0; i < values.length; i++)
            if (values[i] > 0)
                sum += values[i];
            else    // oops!
                sum = values[0];
    return sum;
}

This is probably not what was intended. To bind the else clause to the first if, you can use braces to create blocks:

public double sumPositive(double[] values) {
    double sum = 0.0;

    if (values.length > 1) {
        for (int i = 0; i < values.length; i++)
            if (values[i] > 0)
                sum += values[i];
    } else {
        sum = values[0];
    }
    return sum;
}

Exercise 10.1Using if–else in a loop, write a method that takes a string parameter and returns a string with all the special characters in the original string replaced by their language equivalents. For example, a string with a " in the middle of it should create a return value with that " replaced by ". (Section 7.2.3 on page 167 lists all special characters).

switch

A switch statement allows you to transfer control to a labeled entry point in a block of statements, based on the value of an expression. The general form of a switch statement is:

switch (expression) {
    case n: statements
    case m: statements
    . . .
    default: statements
}

The expression must either be of an integer type (char, byte, short, or int, or a corresponding wrapper class) or an enum type. The body of the switch statement, known as the switch block, contains statements that can be prefixed with case labels. A case label is an integer or enum constant. If the value of the switch expression matches the value of a case label then control is transferred to the first statement following that label. If a matching case label is not found, control is transferred to the first statement following a default label. If there is no default label, the entire switch statement is skipped.

For example, consider our Verbose interface from page 121:

interface Verbose {
    int SILENT  = 0;
    int TERSE   = 1;
    int NORMAL  = 2;
    int VERBOSE = 3;

    void setVerbosity(int level);
    int getVerbosity();
}

Depending on the verbosity level, the state of the object is dumped, adding new output at greater verbosity levels and then printing the output of the next lower level of verbosity:

Verbose v = ... ; // initialized as appropriate
public void dumpState() {
    int verbosity = v.getVerbosity();
    switch (verbosity) {
      case Verbose.SILENT:
        break; // do nothing

      case Verbose.VERBOSE:
        System.out.println(stateDetails);
        // FALLTHROUGH

      case Verbose.NORMAL:
        System.out.println(basicState);
        // FALLTHROUGH

      case Verbose.TERSE:
        System.out.println(summaryState);
        break;

      default:
        throw new IllegalStateException(
                      "verbosity=" + verbosity);
    }
}

Once control has transferred to a statement following a case label, the following statements are executed in turn, according to the semantics of those statements, even if those statements have their own different case labels. The FALLTHROUGH comments in the example show where control falls through the next case label to the code below. Thus, if the verbosity level is VERBOSE, all three output parts are printed; if NORMAL, two parts are printed; and if TERSE, only one part is printed.

A case or default label does not force a break out of the switch. Nor does it imply an end to execution of statements. If you want to stop executing statements in the switch block you must explicitly transfer control out of the switch block. You can do this with a break statement. Within a switch block, a break statement transfers control to the first statement after the switch. This is why we have a break statement after the TERSE output is finished. Without the break, execution would continue through into the code for the default label and throw the exception every time. Similarly, in the SILENT case, all that is executed is the break because there is nothing to print.

Falling through to the next case can be useful in some circumstances. But in most cases a break should come after the code that a case label selects. Good coding style suggests that you always use some form of FALLTHROUGH comment to document an intentional fall-through.

A single statement can have more than one case label, allowing a singular action in multiple cases. For example, here we use a switch statement to decide how to translate a hexadecimal digit into an int:

public int hexValue(char ch) throws NonHexDigitException {
    switch (ch) {
      case '0': case '1': case '2': case '3': case '4':
      case '5': case '6': case '7': case '8': case '9':
        return (ch - '0'),

      case 'a': case 'b': case 'c':
      case 'd': case 'e': case 'f':
        return (ch - 'a') + 10;

      case 'A': case 'B': case 'C':
      case 'D': case 'E': case 'F':
        return (ch - 'A') + 10;

      default:
        throw new NonHexDigitException(ch);
    }
}

There are no break statements because the return statements exit the switch block (and the whole method) before control can fall through.

You should terminate the last group of statements in a switch with a break, return, or throw, as you would a group of statements in an earlier case. Doing so reduces the likelihood of accidentally falling through the bottom of what used to be the last part of the switch when a new case is added.

All case labels must be enum constants or constant expressions—the expressions must contain only literals or named constants initialized with constant expressions—and must be assignable to the type of the switch expression. In any single switch statement, each case value must be unique, and there can be at most one default label.

When you use an enum as a switch expression, you will get no warning if you leave out a case. It is your job to ensure that each case is covered. To safeguard against an enum being redefined to have additional constants, you should always include a default case that simply throws an exception.

Only statements directly within the switch block can have case labels. This means that you cannot, for example, transfer control to a statement within the switch block that is in the middle of a loop, or a statement within a nested block of code. You can, however, skip the declaration and initialization of a local variable within the switch block (not that we recommend it). The effect of this is that the local variable is still considered to be declared, but the initializer has not been executed. Because local variables must be initialized, any attempt to read the value of that variable will result in a compile-time error—the first use of that variable must be an assignment.

The first statement in a switch block must be labeled, otherwise it is unreachable (all cases will jump over it) and your code will not compile.

Exercise 10.2Rewrite your method from Exercise 10.1 to use a switch.

Exercise 10.3Using your “days of the week” enum from Exercise 6.1 write a method that takes a day of the week and returns true if it is a working day, and false otherwise. First use nested ifelse statements and then a switch statement. Which do you think results in clearer code?

while and dowhile

The while loop looks like this:

while (expression)
    statement

The expression—again either of boolean or Boolean type—is evaluated and, if it is true, the statement (which may be a block) is executed. Once the statement completes, the expression is reevaluated and, if still true, the statement is executed again. This repeats until the expression evaluates to false, at which point control transfers to after the while.

We introduced the while loop with our second program in Chapter 1, the Fibonacci program:

while (hi < MAX) {
    System.out.println(hi);
    hi = lo + hi;
    lo = hi - lo;
}

This loops around printing and calculating new Fibonacci values until the highest value computed exceeds the maximum limit.

A while loop executes zero or more times since the expression might be false the first time it is evaluated. Sometimes you want to execute a loop body at least once, which is why you also have a dowhile loop:

do
    statement
while (expression);

Here, the expression is evaluated after the statement is executed. While the expression is true, the statement is executed repeatedly. The statement in a dowhile loop is almost always a block.

Exercise 10.4Select a few previous exercise solutions for which you have used a for loop and rewrite it using a while loop. Can you also rewrite it using a dowhile loop? Would you do so? If not, why not?

for

The for statement comes in two forms: a basic and an enhanced form. The basic form is the more general and powerful of the two.

Basic for Statement

You use the basic for statement to loop over a range of values from beginning to end. It looks like this:

for (initialization-expression;
     loop-expression;
     update-expression)
    statement

The initialization-expression allows you to declare and/or initialize loop variables, and is executed only once. Then the loop-expression—of boolean or Boolean type—is evaluated and if it is true the statement in the body of the loop is executed. After executing the loop body, the update-expression is evaluated, usually to update the values of the loop variables, and then the loop-expression is reevaluated. This cycle repeats until the loop-expression is found to be false. This is roughly equivalent to

{
    initialization-expression;
    while (loop-expression) {
        statement
        update-expression;
    }
}

except that in a for statement the update-expression is always executed if a continue is encountered in the loop body (see “continue” on page 244).

The initialization and update parts of a for loop can be comma-separated lists of expressions. The expressions separated by the commas are, like most operator operands, evaluated from left-to-right. For example, to march two indexes through an array in opposite directions, the following code would be appropriate:

for (i = 0, j = arr.length - 1; j >= 0; i++, j--) {
    // ...
}

The initialization section of a for loop can also be a local variable declaration statement, as described on page 170. For example, if i and j are not used outside the for loop, you could rewrite the previous example as:

for (int i = 0, j = arr.length - 1; j >= 0; i++, j--) {
    // ...
}

If you have a local variable declaration, however, each part of the expression after a comma is expected to be a part of that local variable declaration. For example, if you want to print the first MAX members of a linked list you need to both maintain a count and iterate through the list members. You might be tempted to try the following:

for (int i = 0, Cell node = head;             // INVALID
     i < MAX && node != null;
     i++, node = node.next)
{
    System.out.println(node.getElement());
}

This will not compile: In a variable declaration the comma separates the different variables being declared, and Cell is a type, not a variable. Declarations of different types of variables are distinct statements terminated by semicolons. If you change the comma to a semicolon, however, you get a for loop with four sections, not three—still an error. If you need to initialize two different types of variables then neither of them can be declared within the for loop:

int i;
Cell node;
for (i = 0, node = head;
     i < MAX && node != null;
     i++, node = node.next)
{
    System.out.println(node.getElement());
}

Typically, the for loop is used to iterate a variable over a range of values until some logical end to that range is reached. You can define what an iteration range is. A for loop is often used, for example, to iterate through the elements of a linked list or to follow a mathematical sequence of values. This capability makes the for construct more powerful than equivalent constructs in many other languages that restrict for-style constructs to incrementing a variable over a range of values.

Here is an example of such a loop, designed to calculate the smallest value of exp such that 10exp is greater than or equal to a value:

public static int tenPower(int value) {
    int exp, v;
    for (exp = 0, v = value - 1; v > 0; exp++, v /= 10)
        continue;
    return exp;
}

In this case, two variables step through different ranges. As long as the loop variable v is greater than zero, the exponent value is incremented and v is divided by ten. When the loop completes, the value 10exp is the smallest power of ten that is greater than or equal to value. Both the test value and the exponent are updated on each loop iteration. In such cases, a comma-separated list of expressions is a good technique to ensure that the two values are always in lockstep.

The body of this loop is simply a continue statement, which starts the next iteration of the loop. The body of the loop has nothing to do—all the work of the loop is in the test and iteration clauses of the for statement itself. The continue style shown here is one way to show an empty loop body; another way is to put a simple semicolon on a line by itself or to use an empty block with braces. Simply putting a semicolon at the end of the for line is dangerous—if the semicolon is accidentally deleted or forgotten, the statement that follows the for can silently become the body of the for.

All the expressions in the for construct are optional. If initialization-expression or update-expression is left out, its part in the loop is simply omitted. If loop-expression is left out, it is assumed to be true. Thus, one idiomatic way to write an infinite loop is as a “for ever” loop:

for (;;)
    statement

Presumably, the loop is terminated by some other means, such as a break statement (described later) or by throwing an exception.

Conventionally, the for loop is used only when looping through a range of related values. It is bad style to violate this convention by using initialization or increment expressions that are unrelated to the boolean loop test.

Exercise 10.5Write a method that takes two char parameters and prints the characters between those two values, including the endpoints.

Enhanced for Statement

The enhanced for statement, often referred to as the for-each loop, provides an alternative, more compact form for iterating through a set of values. It looks like this:

for (Type loop-variable : set-expression)
     statement

The set-expression must evaluate to an object that defines the set of values that you want to iterate through, and the loop-variable is a local variable of a suitable type for the set's contents. Each time through the loop, loop-variable takes on the next value from the set, and statement is executed (presumably using the loop-variable for something). This continues until no more values remain in the set.

The set-expression must either evaluate to an array instance, or an object that implements the interface java.lang.Iterable—which is the case for all of the collection classes you'll see in Chapter 21. Here's an example using an array, where we rewrite the average method we showed on page 19:

static double average(int[] values) {
    if (values == null || values.length == 0)
        throw new IllegalArgumentException();

    double sum = 0.0;
    for (int val : values)
        sum += val;

    return sum / values.length;
}

The for statement is read “for each i in values” and each time through the loop we add the next value, i, to the sum. This is equivalent to a basic for statement written as follows:

for (int j = 0 ; j < values.length; j++) {
    int val = values[j];
    sum += val;
}

The advantage of using the for-each loop with arrays is that you don't have to manually maintain the array index and check the array length. The disadvantage is that for-each can only loop forwards through a single array, and only looks at the array elements. If you want to modify an array element you will need to know what index you are working with, and that information is not available in the enhanced for statement.

The main motivation behind the enhanced for statement is to make it more convenient to iterate through collection classes, or more generally anything that implements the Iterable interface. The Iterable interface defines a single method iterator that returns an Iterator for that object (see “Iteration” on page 571). Recalling the AttributedImpl class we defined in Chapter 4, we can define a method to print all the attributes of an AttributedImpl object:

static void printAttributes(AttributedImpl obj) {
    for (Attr attr : obj)
        System.out.println(attr);
}

Again we read this as “for each attr in obj” and it is simply a syntactic shorthand for a more verbose basic for statement that uses the iterator explicitly:

for (Iterator<Attr> iter = obj.iterator();
     iter.hasNext();
     /* no update expression */)
{
    Attr attr = iter.next();
    System.out.println(attr);
}

As before the advantage of the enhanced for is a simple, syntactic construct for accessing the elements you are iterating through. But you cannot use the iterator's remove method to alter the collection, nor can you iterate through multiple collections at the same time. If you need these capabilities you must use the basic for statement and an explicit iterator.

Labels

You can label statements to give them a name by which they can be referred. A label precedes the statement it names; only one label is allowed per statement:

label: statement

Labels can be referred to only by the break and continue statements (discussed next).

break

A break statement can be used to exit from any block, not just from a switch. There are two forms of break statement. The unlabeled break:

break;

and the labeled break:

break label;

An unlabeled break terminates the innermost switch, for, while, or do statement—and so can appear only within one of those statements. A labeled break can terminate any labeled statement.

A break is most often used to break out of a loop. In this example, we are looking for the first empty slot in an array of references to Contained objects:

class Container {
    private Contained[] objs;

    // ...

    public void addIn(Contained obj)
        throws NoEmptySlotException
    {
        int i;
        for (i = 0; i < objs.length; i++)
            if (objs[i] == null)
                break;
        if (i >= objs.length)
            throw new NoEmptySlotException();
        objs[i] = obj;    // put it inside me
        obj.inside(this); // let it know it's inside me
    }
}

To terminate an outer loop or block, you label the outer statement and use its label name in the break statement:

private float[][] matrix;

public boolean workOnFlag(float flag) {
    int y, x;
    boolean found = false;

  search:
    for (y = 0; y < matrix.length; y++) {
        for (x = 0; x < matrix[y].length; x++) {
            if (matrix[y][x] == flag) {
                found = true;
                break search;
            }
        }
    }
    if (!found)
        return false;

    // do some stuff with flagged value at matrix[y][x]
    return true;
}

Here we label the outer for loop and if we find the value we are looking for, we terminate both inner and outer loops by using a labeled break. This simplifies the logic of the loops because we do not need to add a !found clause in the loop expressions.

Note that a labeled break is not a goto. The goto statement would enable indiscriminate jumping around in code, obfuscating the flow of control. A break or continue that references a label, on the other hand, exits from or repeats only that specific labeled block, and the flow of control is obvious by inspection. For example, here is a modified version of workOnFlag that labels a block instead of a loop, allowing us to dispense with the found flag altogether:

public boolean workOnFlag(float flag) {
    int y, x;

  search:
    {
        for (y = 0; y < matrix.length; y++) {
            for (x = 0; x < matrix[y].length; x++) {
                if (matrix[y][x] == flag)
                    break search;
            }
        }

        // if we get here we didn't find it
        return false;
    }
    // do some stuff with flagged value at matrix[y][x]
    return true;
}

You should use the break statement judiciously, and with thought to the clarity of the resulting code. Arbitrarily jumping out of loops or statements can obscure the flow of control in the code, making it harder to understand and maintain. Some programming styles ban the use of break altogether, but that is an extreme position. Contorting the logic in a loop to avoid the need for a break can result in code that is less clear than it would have been with the break.

continue

A continue statement can be used only within a loop (for, while, or do) and transfers control to the end of the loop's body to continue on with the loop. In the case of while and do loops, this causes the loop expression to be the next thing evaluated. In a basic for loop continue causes the update-expression to be evaluated next and then the loop expression. In the enhanced for loop execution goes to the next element in the set of values if there is one.

Like the break statement, the continue statement has an unlabeled form:

continue;

and a labeled form:

continue label;

In the unlabeled form, continue transfers control to the end of the innermost loop's body. The labeled form transfers control to the end of the loop with that label. The label must belong to a loop statement.

A continue is often used to skip over an element of a loop range that can be ignored or treated with trivial code. For example, a token stream that included a simple “skip” token might be handled this way:

while (!stream.eof()) {
    token = stream.next();
    if (token.equals("skip"))
        continue;
    // ... process token ...
}

A labeled continue will break out of any inner loops on its way to the next iteration of the named loop. No label is required on the continue in the preceding example since there is only one enclosing loop. Consider, however, nested loops that iterate over the values of a two-dimensional matrix. Suppose that the matrix is symmetric (matrix[i][j]== matrix[j][i]). In that case, you need only iterate through half of the matrix. For example, here is a method that doubles each value in a symmetric matrix:

static void doubleUp(int[][] matrix) {
    int order = matrix.length;
  column:
    for (int i = 0; i < order; i++) {
        for (int j = 0; j < order; j++) {
            matrix[i][j] = matrix[j][i] = matrix[i][j]*2;
            if (i == j)
                continue column;
        }
    }
}

Each time a diagonal element of the matrix is reached, the rest of that row is skipped by continuing with the outer loop that iterates over the columns.

return

A return statement terminates execution of a method and returns to the invoker. If the method returns no value, a simple return statement will do:

return;

If the method has a return type, the return must include an expression of a type that could be assigned to the return type. For example, if a method returns double, a return could have an expression that was a double, float, or integer:

protected double nonNegative(double val) {
    if (val < 0)
        return 0;   // an int constant
    else
        return val; // a double
}

A return can also be used to exit a constructor. Since constructors do not specify a return type, return is used without a value. Constructors are invoked as part of the new process that, in the end, returns a reference to a new object, but each constructor plays only a part of that role; no constructor “returns” the final reference.

What, No goto?

The Java programming language has no goto construct that transfers control to an arbitrary statement, although goto is common in languages to which the language is related.[2] The primary uses for goto in such languages have other solutions:

  • Controlling outer loops from within nested loops. Use labeled break and continue statements to meet this need.

  • Skipping the rest of a block of code that is not in a loop when an answer or error is found. Use a labeled break.

  • Executing cleanup code before a method or block of code exits. Use either a labeled break or, more cleanly, the finally construct of the try statement covered in Chapter 12.

Labeled break and continue have the advantage that they transfer control to a strictly limited place. A finally block is even stricter as to where it transfers control, and it works in all circumstances, including exceptions. With these constructs you can write clean code without a goto.

 

Furious activity is no substitute for understanding.

 
 --H.H. Williams


[1] There is a distinction between terminator and separator. The comma between identifiers in declarations is a separator because it comes between elements in the list. The semicolon is a terminator because it ends each statement. If the semicolon were a statement separator, the last semicolon in a code block would be unnecessary and (depending on the choice of the language designer) possibly invalid.

[2] Although not used goto is a reserved keyword, as is const. The reason they are reserved is mostly historical: Both of these come from strongly related programming languages, like C and C++, and reserving them helped compilers advise programmers clearly that they were using something that didn't make sense. Occasionally, suggestions are made for how const might be used in the Java programming language.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.146.35.72