A statement is a single "command" that is executed by the Java interpreter. By default, the Java interpreter runs one statement after another, in the order they are written. Many of the statements defined by Java, however, are flow-control statements, such as conditionals and loops, that alter this default order of execution in well-defined ways. Table 2-6 summarizes the statements defined by Java.
As we saw earlier in the chapter, certain types of Java expressions have side effects. In other words, they do not simply evaluate to some value, but also change the program state in some way. Any expression with side effects can be used as a statement simply by following it with a semicolon. The legal types of expression statements are assignments, increments and decrements, method calls, and object creation. For example:
a = 1; // Assignment x *= 2; // Assignment with operation i++; // Post-increment --c; // Pre-decrement System.out.println("statement"); // Method invocation
A compound statement is any number and kind of statements grouped together within curly braces. You can use a compound statement anywhere a statement is required by Java syntax:
for(int i = 0; i < 10; i++) { a[i]++; // Body of this loop is a compound statement. b[i]--; // It consists of two expression statements } // within curly braces.
An empty statement in Java is written as a single semicolon. The empty statement doesn't do anything, but the syntax is occasionally useful. For example, you can use it to indicate an empty loop body of a for loop:
for(int i = 0; i < 10; a[i++]++) // Increment array elements /* empty */; // Loop body is empty statement
A labeled statement is simply a statement that has been given a name by prepending a identifier and a colon to it. Labels are used by the break and continue statements. For example:
rowLoop: for(int r = 0; r < rows.length; r++) { // A labeled loop colLoop: for(int c = 0; c < columns.length; c++) { // Another one break rowLoop; // Use a label } }
A local variable, often simply called a variable, is a symbolic name for a location where a value can be stored that is defined within a method or compound statement. All variables must be declared before they can be used; this is done with a variable declaration statement. Because Java is a strongly typed language, a variable declaration specifies the type of the variable, and only values of that type can be stored in the variable.
In its simplest form, a variable declaration specifies a variable's type and name:
int counter; String s;
A variable declaration can also include an initializer : an expression that specifies an initial value for the variable. For example:
int i = 0; String s = readLine(); int[] data = {x+1, x+2, x+3}; // Array initializers are documented later
The Java compiler does not allow you to use a variable that has not been initialized, so it is usually convenient to combine variable declaration and initialization into a single statement. The initializer expression need not be a literal value or a constant expression that can be evaluated by the compiler; it can be an arbitrarily complex expression whose value is computed when the program is run.
A single variable declaration statement can declare and initialize more than one variable, but all variables must be of the same type. Variable names and optional initializers are separated from each other with commas:
int i, j, k; float x = 1.0, y = 1.0; String question = "Really Quit?", response;
In Java 1.1 and later, variable declaration statements can begin with the final keyword. This modifier specifies that once an initial value is specified for the variable, that value is never allowed to change:
final String greeting = getLocalLanguageGreeting();
C programmers should note that Java variable declaration statements can appear anywhere in Java code; they are not restricted to the beginning of a method or block of code. Local variable declarations can also be integrated with the initialize portion of a for loop, as we'll discuss shortly.
Local variables can be used only within the method or block of code in which they are defined. This is called their scope or lexical scope :
void method() { // A generic method int i = 0; // Declare variable i while (i < 10) { // i is in scope here int j = 0; // Declare j; i and j are in scope here } // j is no longer in scope; can't use it anymore System.out.println(i); // i is still in scope here } // The scope of i ends here
The if statement is the fundamental control statement that allows Java to make decisions or, more precisely, to execute statements conditionally. The if statement has an associated expression and statement. If the expression evaluates to true, the interpreter executes the statement. If the expression evaluates to false, however, the interpreter skips the statement. For example:
if (username == null) // If username is null, username = "John Doe"; // define it.
Although they look extraneous, the parentheses around the expression are a required part of the syntax for the if statement.
As I already mentioned, a block of statements enclosed in curly braces is itself a statement, so we can also write if statements that look as follows:
if ((address == null) || (address.equals(""))) { address = "[undefined]"; System.out.println("WARNING: no address specified."); }
An if statement can include an optional else keyword that is followed by a second statement. In this form of the statement, the expression is evaluated, and, if it is true, the first statement is executed. Otherwise, the second statement is executed. For example:
if (username != null) System.out.println("Hello " + username); else { username = askQuestion("What is your name?"); System.out.println("Hello " + username + ". Welcome!"); }
When you use nested if/else statements, some caution is required to ensure that the else clause goes with the appropriate if statement. Consider the following lines:
if (i == j) if (j == k) System.out.println("i equals k"); else System.out.println("i doesn't equal j"); // WRONG!!
In this example, the inner if statement forms the single statement allowed by the syntax of the outer if statement. Unfortunately, it is not clear (except from the hint given by the indentation) which if the else goes with. And in this example, the indentation hint is wrong. The rule is that an else clause like this is associated with the nearest if statement. Properly indented, this code looks like this:
if (i == j) if (j == k) System.out.println("i equals k"); else System.out.println("i doesn't equal j"); // WRONG!!
This is legal code, but it is clearly not what the programmer had in mind. When working with nested if statements, you should use curly braces to make your code easier to read. Here is a better way to write the code:
if (i == j) { if (j == k) System.out.println("i equals k"); } else { System.out.println("i doesn't equal j"); }
The if/else statement is useful for testing a condition and choosing between two statements or blocks of code to execute. But what about when you need to choose between several blocks of code? This is typically done with an elseif clause, which is not really new syntax, but a common idiomatic usage of the standard if/else statement. It looks like this:
if (n == 1) { // Execute code block #1 } else if (n == 2) { // Execute code block #2 } else if (n == 3) { // Execute code block #3 } else { // If all else fails, execute block #4 }
There is nothing special about this code. It is just a series of if statements, where each if is part of the else clause of the previous statement. Using the elseif idiom is preferable to, and more legible than, writing these statements out in their fully nested form:
if (n == 1) { // Execute code block #1 } else { if (n == 2) { // Execute code block #2 } else { if (n == 3) { // Execute code block #3 } else { // If all else fails, execute block #4 } } }
An if statement causes a branch in the flow of a program's execution. You can use multiple if statements, as shown in the previous section, to perform a multiway branch. This is not always the best solution, however, especially when all of the branches depend on the value of a single variable. In this case, it is inefficient to repeatedly check the value of the same variable in multiple if statements.
A better solution is to use a switch statement, which is inherited from the C programming language. Although the syntax of this statement is not nearly as elegant as other parts of Java, the brute practicality of the construct makes it worthwhile. If you are not familiar with the switch statement itself, you may at least be familiar with the basic concept, under the name computed goto or jump table. A switch statement has an integer expression and a body that contains various numbered entry points. The expression is evaluated, and control jumps to the entry point specified by that value. For example, the following switch statement is equivalent to the repeated if and else/if statements shown in the previous section:
switch(n) { case 1: // Start here if n == 1 // Execute code block #1 break; // Stop here case 2: // Start here if n == 2 // Execute code block #2 break; // Stop here case 3: // Start here if n == 3 // Execute code block #3 break; // Stop here default: // If all else fails... // Execute code block #4 break; // Stop here }
As you can see from the example, the various entry points into a switch statement are labeled either with the keyword case, followed by an integer value and a colon, or with the special default keyword, followed by a colon. When a switch statement executes, the interpreter computes the value of the expression in parentheses and then looks for a case label that matches that value. If it finds one, the interpreter starts executing the block of code at the first statement following the case label. If it does not find a case label with a matching value, the interpreter starts execution at the first statement following a special-case default: label. Or, if there is no default: label, the interpreter skips the body of the switch statement altogether.
Note the use of the break keyword at the end of each case in the previous code. The break statement is described later in this chapter, but, in this case, it causes the interpreter to exit the body of the switch statement. The case clauses in a switch statement specify only the starting point of the desired code. The individual cases are not independent blocks of code, and they do not have any implicit ending point. Therefore, you must explicitly specify the end of each case with a break or related statement. In the absence of break statements, a switch statement begins executing code at the first statement after the matching case label and continues executing statements until it reaches the end of the block. On rare occasions, it is useful to write code like this that falls through from one case label to the next, but 99% of the time you should be careful to end every case and default section with a statement that causes the switch statement to stop executing. Normally you use a break statement, but return and throw also work.
A switch statement can have more than one case clause labeling the same statement. Consider the switch statement in the following method:
boolean parseYesOrNoResponse(char response) { switch(response) { case 'y': case 'Y': return true; case 'n': case 'N': return false; default: throw new IllegalArgumentException("Response must be Y or N"); } }
There are some important restrictions on the switch statement and its case labels. First, the expression associated with a switch statement must have a byte, char, short, or int value. The floating-point and boolean types are not supported, and neither is long, even though long is an integer type. Second, the value associated with each case label must be a constant value or a constant expression the compiler can evaluate. A case label cannot contain a runtime expressions involving variables or method calls, for example. Third, the case label values must be within the range of the data type used for the switch expression. And finally, it is obviously not legal to have two or more case labels with the same value or more than one default label.
Just as the if statement is the basic control statement that allows Java to make decisions, the while statement is the basic statement that allows Java to perform repetitive actions. It has the following syntax:
while (expression) statement
The while statement works by first evaluating the expression. If it is false, the interpreter skips the statement associated with the loop and moves to the next statement in the program. If it is true, however, the statement that forms the body of the loop is executed, and the expression is reevaluated. Again, if the value of expression is false, the interpreter moves on to the next statement in the program; otherwise it executes the statement again. This cycle continues while the expression remains true (i.e., until it evaluates to false), at which point the while statement ends, and the interpreter moves on to the next statement. You can create an infinite loop with the syntax while(true).
Here is an example while loop that prints the numbers 0 to 9:
int count = 0; while (count < 10) { System.out.println(count); count++; }
As you can see, the variable count starts off at 0 in this example and is incremented each time the body of the loop runs. Once the loop has executed 10 times, the expression becomes false (i.e., count is no longer less than 10), the while statement finishes, and the Java interpreter can move to the next statement in the program. Most loops have a counter variable like count. The variable names i, j, and k are commonly used as a loop counters, although you should use more descriptive names if it makes your code easier to understand.
A do loop is much like a while loop, except that the loop expression is tested at the bottom of the loop, rather than at the top. This means that the body of the loop is always executed at least once. The syntax is:
do statement while ( expression ) ;
There are a couple of differences to notice between the do loop and the more ordinary while loop. First, the do loop requires both the do keyword to mark the beginning of the loop and the while keyword to mark the end and introduce the loop condition. Also, unlike the while loop, the do loop is terminated with a semicolon. This is because the do loop ends with the loop condition, rather than simply ending with a curly brace that marks the end of the loop body. The following do loop prints the same output as the while loop shown above:
int count = 0; do { System.out.println(count); count++; } while(count < 10);
Note that the do loop is much less commonly used than its while cousin. This is because, in practice, it is unusual to encounter a situation where you are sure you always want a loop to execute at least once.
The for statement provides a looping construct that is often more convenient than the while and do loops. The for statement takes advantage of a common looping pattern. Most loops have a counter, or state variable of some kind, that is initialized before the loop starts, tested to determine whether to execute the loop body, and then incremented, or updated somehow, at the end of the loop body before the test expression is evaluated again. The initialization, test, and update steps are the three crucial manipulations of a loop variable, and the for statement makes these three steps an explicit part of the loop syntax:
for(initialize ; test ; increment) statement
This for loop is basically equivalent to the following while loop:[2]
[2] As you'll see when we consider the continue statement, this while loop is not exactly equivalent to the for loop. We'll discuss how to write the true equivalent when we talk about the try/catch/finally statement.
initialize; while(test) { statement; increment; }
Placing the initialize, test, and increment expressions at the top of a for loop makes it especially easy to understand what the loop is doing, and it prevents mistakes such as forgetting to initialize or increment the loop variable. The interpreter discards the values of the initialize and increment expressions, so in order to be useful, these expressions must have side effects. initialize is typically an assignment expression, while increment is usually an increment, decrement, or some other assignment.
The following for loop prints the numbers 0 to 9, just as the previous while and do loops have done:
int count; for(count = 0 ; count < 10 ; count++) System.out.println(count);
Notice how this syntax places all the important information about the loop variable on a single line, making it very clear how the loop executes. Placing the increment expression in the for statement itself also simplifies the body of the loop to a single statement; we don't even need to use curly braces to produce a statement block.
The for loop supports some additional syntax that makes it even more convenient to use. Because many loops use their loop variables only within the loop, the for loop allows the initialize expression to be a full variable declaration, so that the variable is scoped to the body of the loop and is not visible outside of it. For example:
for(int count = 0 ; count < 10 ; count++) System.out.println(count);
Furthermore, the for loop syntax does not restrict you to writing loops that use only a single variable. Both the initialize and increment expressions of a for loop can use a comma to separate multiple initializations and increment expressions. For example:
for(int i = 0, j = 10 ; i < 10 ; i++, j--) sum += i * j;
Even though all the examples so far have counted numbers, for loops are not restricted to loops that count numbers. For example, you might use a for loop to iterate through the elements of a linked list:
for(Node n = listHead; n != null; n = n.nextNode()) process(n);
The initialize, test, and increment expressions of a for loop are all optional; only the semicolons that separate the expressions are required. If the test expression is omitted, it is assumed to be true. Thus, you can write an infinite loop as for(;;).
A break statement causes the Java interpreter to skip immediately to the end of a containing statement. We have already seen the break statement used with the switch statement. The break statement is most often written as simply the keyword break followed by a semicolon:
break;
When used in this form, it causes the Java interpreter to immediately exit the innermost containing while, do, for, or switch statement. For example:
for(int i = 0; i < data.length; i++) { // Loop through the data array. if (data[i] == target) { // When we find what we're looking for, index = i; // remember where we found it break; // and stop looking! } } // The Java interpreter goes here after executing break
The break statement can also be followed by the name of a containing labeled statement. When used in this form, break causes the Java interpreter to immediately exit from the named block, which can be any kind of statement, not just a loop or switch. For example:
testfornull: if (data != null) { // If the array is defined, for(int row = 0; row < numrows; row++) { // loop through one dimension, for(int col = 0; col < numcols; col++) { // then loop through the other. if (data[row][col] == null) // If the array is missing data, break testfornull; // treat the array as undefined. } } } // Java interpreter goes here after executing break testfornull
While a break statement exits a loop, a continue statement quits the current iteration of a loop and starts the next one. continue, in both its unlabeled and labeled forms, can be used only within a while, do, or for loop. When used without a label, continue causes the innermost loop to start a new iteration. When used with a label that is the name of a containing loop, it causes the named loop to start a new iteration. For example:
for(int i = 0; i < data.length; i++) { // Loop through data. if (data[i] == -1) // If a data value is missing, continue; // skip to the next iteration. process(data[i]); // Process the data value. }
while, do, and for loops differ slightly in the way that continue starts a new iteration:
With a while loop, the Java interpreter simply returns to the top of the loop, tests the loop condition again, and, if it evaluates to true, executes the body of the loop again.
With a do loop, the interpreter jumps to the bottom of the loop, where it tests the loop condition to decide whether to perform another iteration of the loop.
With a for loop, the interpreter jumps to the top of the loop, where it first evaluates the increment expression and then evaluates the test expression to decide whether to loop again. As you can see, the behavior of a for loop with a continue statement is different from the behavior of the "basically equivalent" while loop I presented earlier; increment gets evaluated in the for loop, but not in the equivalent while loop.
A return statement tells the Java interpreter to stop executing the current method. If the method is declared to return a value, the return statement is followed by an expression. The value of the expression becomes the return value of the method. For example, the following method computes and returns the square of a number:
double square(double x) { // A method to compute x squared return x * x; // Compute and return a value }
Some methods are declared void to indicate they do not return any value. The Java interpreter runs methods like this by executing its statements one by one until it reaches the end of the method. After executing the last statement, the interpreter returns implicitly. Sometimes, however, a void method has to return explicitly before reaching the last statement. In this case, it can use the return statement by itself, without any expression. For example, the following method prints, but does not return, the square root of its argument. If the argument is a negative number, it returns without printing anything:
void printSquareRoot(double x) { // A method to print square root of x if (x < 0) return; // If x is negative, return explicitly System.out.println(Math.sqrt(x)); // Print the square root of x } // End of method: return implicitly
Since Java is a multithreaded system, you must often take care to prevent multiple threads from modifying an object simultaneously in a way that might corrupt the object's state. Sections of code that must not be executed simultaneously are known as critical sections. Java provides the synchronized statement to protect these critical sections. The syntax is:
synchronized ( expression ) { statements }
expression is an expression that must evaluate to an object or an array. The statements constitute the code of the critical section and must be enclosed in curly braces. Before executing the critical section, the Java interpreter first obtains an exclusive lock on the object or array specified by expression. It holds the lock until it is finished running the critical section, then releases it. While a thread holds the lock on an object, no other thread can obtain that lock. Therefore, no other thread can execute this or any other critical sections that require a lock on the same object. If a thread cannot immediately obtain the lock required to execute a critical section, it simply waits until the lock becomes available.
Note that you do not have to use the synchronized statement unless your program creates multiple threads that share data. If only one thread ever accesses a data structure, there is no need to protect it with synchronized. When you do have to use synchronized, it might be in code like the following:
public static void SortIntArray(int[] a) { // Sort the array a. This is synchronized so that some other thread // cannot change elements of the array while we're sorting it (at // least not other threads that protect their changes to the array // with synchronized). synchronized (a) { // Do the array sort here... } }
The synchronized keyword is also available as a modifier in Java and is more commonly used in this form than as a statement. When applied to a method, the synchronized keyword indicates that the entire method is a critical section. For a synchronized class method (a static method), Java obtains an exclusive lock on the class before executing the method. For a synchronized instance method, Java obtains an exclusive lock on the class instance. (Class and instance methods are discussed in Chapter 3, "Object-Oriented Programming in Java".)
An exception is a signal that indicates some sort of exceptional condition or error has occurred. To throw an exception is to signal an exceptional condition. To catch an exception is to handle it--to take whatever actions are necessary to recover from it.
In Java, the throw statement is used to throw an exception:
throw expression ;
The expression must evaluate to an exception object that describes the exception or error that has occurred. We'll talk more about types of exceptions shortly; for now, all you need to know is that an exception is represented by an object. Here is some example code that throws an exception:
public static double factorial(int x) { if (x < 0) throw new IllegalArgumentException("x must be >= 0")); double fact; for(fact=1.0; x > 1; fact *= x, x--) /* empty */ ; // Note use of the empty statement return fact; }
When the Java interpreter executes a throw statement, it immediately stops normal program execution and starts looking for an exception handler that can catch, or handle, the exception. Exception handlers are written with the try/catch/finally statement, which is described in the next section. The Java interpreter first looks at the enclosing block of code to see if it has an associated exception handler. If so, it exits that block of code and starts running the exception-handling code associated with the block. After running the exception handler, the interpreter continues execution at the statement immediately following the handler code.
If the enclosing block of code does not have an appropriate exception handler, the interpreter checks the next higher enclosing block of code in the method. This continues until a handler is found. If the method does not contain an exception handler that can handle the exception thrown by the throw statement, the interpreter stops running the current method and returns to the caller. Now the interpreter starts looking for an exception handler in the blocks of code of the calling method. In this way, exceptions propagate up through the lexical structure of Java methods, up the call stack of the Java interpreter. If the exception is never caught, it propagates all the way up to the main() method of the program. If it is not handled in that method, the Java interpreter prints an error message, prints a stack trace to indicate where the exception occurred, and then exits.
An exception in Java is an object. The type of this object is java.langThrowable, or more commonly, some subclass of Throwable that more specifically describes the type of exception that occurred.[3]Throwable has two standard subclasses: java.lang.Error and java.lang.Exception. Exceptions that are subclasses of Error generally indicate unrecoverable problems: the virtual machine has run out of memory, or a class file is corrupted and cannot be read, for example. Exceptions of this sort can be caught and handled, but it is rare to do so. Exceptions that are subclasses of Exception, on the other hand, indicate less severe conditions. These are exceptions that can be reasonably caught and handled. They include such exceptions as java.io.EOFException, which signals the end of a file, and java.lang.ArrayIndexOutOfBoundsException, which indicates that a program has tried to read past the end of an array. In this book, I use the term "exception" to refer to any exception object, regardless of whether the type of that exception is Exception or Error.
[3]We haven't talked about subclasses yet; they are covered in detail in Chapter 3, "Object-Oriented Programming in Java".
Since an exception is an object, it can contain data, and its class can define methods that operate on that data. The Throwable class and all its subclasses include a String field that stores a human-readable error message that describes the exceptional condition. It's set when the exception object is created and can be read from the exception with the getMessage() method. Most exceptions contain only this single message, but a few add other data. The java.io.InterruptedIOException, for example, adds a field named bytesTransferred that specifies how much input or output was completed before the exceptional condition interrupted it.
In addition to making a distinction between Error and Exception classes, the Java exception-handling scheme also makes a distinction between checked and unchecked exceptions. Any exception object that is an Error is unchecked. Any exception object that is an Exception is checked, unless it is a subclass of java.lang.RuntimeException, in which case it is unchecked. (RuntimeException is a subclass of Exception.) The reason for this distinction is that virtually any method can throw an unchecked exception, at essentially any time. There is no way to predict an OutOfMemoryError, for example, and any method that uses objects or arrays can throw a NullPointerException if it is passed an invalid null argument. Checked exceptions, on the other hand, arise only in specific, well-defined circumstances. If you try to read data from a file, for example, you must at least consider the possibility that a FileNotFoundException will be thrown if the specified file cannot be found.
Java has different rules for working with checked and unchecked exceptions. If you write a method that throws a checked exception, you must use a throws clause to declare the exception in the method signature. The reason these types of exceptions are called checked exceptions is that the Java compiler checks to make sure you have declared them in method signatures and produces a compilation error if you have not. The factorial() method shown earlier throws an exception of type java.lang.IllegalArgumentException. This is a subclass of RuntimeException, so it is an unchecked exception, and we do not have to declare it with a throws clause (although we can if we want to be explicit).
Even if you never throw an exception yourself, there are times when you must use a throws clause to declare an exception. If your method calls a method that can throw a checked exception, you must either include exception-handling code to handle that exception or use throws to declare that your method can also throw that exception.
How do you know if the method you are calling can throw a checked exception? You can look at its method signature to find out. Or, failing that, the Java compiler will tell you (by reporting a compilation error) if you've called a method whose exceptions you must handle or declare. The following method reads the first line of text from a named file. It uses methods that can throw various types of java.io.IOException objects, so it declares this fact with a throws clause:
public static String readFirstLine(String filename) throws IOException { BufferedReader in = new BufferedReader(new FileReader(filename)); return in.readLine(); }
We'll talk more about method declarations and method signatures later in this chapter.
The try/catch/finally statement is Java's exception-handling mechanism. The try clause of this statement establishes a block of code for exception handling. This try block is followed by zero or more catch clauses, each of which is a block of statements designed to handle a specific type of exception. The catch clauses are followed by an optional finally block that contains cleanup code guaranteed to be executed regardless of what happens in the try block. Both the catch and finally clauses are optional, but every try block must be accompanied by at least one or the other. The try, catch, and finally blocks all begin and end with curly braces. These are a required part of the syntax and cannot be omitted, even if the clause contains only a single statement.
The following code illustrates the syntax and purpose of the try/catch/finally statement:
try { // Normally this code runs from the top of the block to the bottom // without problems. But it can sometimes throw an exception, // either directly with a throw statement or indirectly by calling // a method that throws an exception. } catch (SomeException e1) { // This block contains statements that handle an exception object // of type SomeException or a subclass of that type. Statements in // this block can refer to that exception object by the name e1. } catch (AnotherException e2) { // This block contains statements that handle an exception object // of type AnotherException or a subclass of that type. Statements // in this block can refer to that exception object by the name e2. } finally { // This block contains statements that are always executed // after we leave the try clause, regardless of whether we leave it: // 1) normally, after reaching the bottom of the block; // 2) because of a break, continue, or return statement; // 3) with an exception that is handled by a catch clause above; or // 4) with an uncaught exception that has not been handled. // If the try clause calls System.exit(), however, the interpreter // exits before the finally clause can be run. }
The try clause simply establishes a block of code that either has its exceptions handled or needs special cleanup code to be run when it terminates for any reason. The try clause by itself doesn't do anything interesting; it is the catch and finally clauses that do the exception-handling and cleanup operations.
A try block can be followed by zero or more catch clauses that specify code to handle various types of exceptions. Each catch clause is declared with a single argument that specifies the type of exceptions the clause can handle and also provides a name the clause can use to refer to the exception object it is currently handling. The type and name of an exception handled by a catch clause are exactly like the type and name of an argument passed to a method, except that for a catch clause, the argument type must be Throwable or one of its subclasses.
When an exception is thrown, the Java interpreter looks for a catch clause with an argument of the same type as the exception object or a superclass of that type. The interpreter invokes the first such catch clause it finds. The code within a catch block should take whatever action is necessary to cope with the exceptional condition. If the exception is a java.io.FileNotFoundException exception, for example, you might handle it by asking the user to check his spelling and try again. It is not required to have a catch clause for every possible exception; in some cases the correct response is to allow the exception to propagate up and be caught by the invoking method. In other cases, such as a programming error signaled by NullPointerException, the correct response is probably not to catch the exception at all, but allow it to propagate and have the Java interpreter exit with a stack trace and an error message.
The finally clause is generally used to clean up after the code in the try clause (e.g., close files, shut down network connections). What is useful about the finally clause is that it is guaranteed to be executed if any portion of the try block is executed, regardless of how the code in the try block completes. In fact, the only way a try clause can exit without allowing the finally clause to be executed is by invoking the System.exit() method, which causes the Java interpreter to stop running.
In the normal case, control reaches the end of the try block and then proceeds to the finally block, which performs any necessary cleanup. If control leaves the try block because of a return, continue, or break statement, the finally block is executed before control transfers to its new destination.
If an exception occurs in the try block, and there is an associated catch block to handle the exception, control transfers first to the catch block and then to the finally block. If there is no local catch block to handle the exception, control transfers first to the finally block, and then propagates up to the nearest containing catch clause that can handle the exception.
If a finally block itself transfers control with a return, continue, break, or throw statement or by calling a method that throws an exception, the pending control transfer is abandoned, and this new transfer is processed. For example, if a finally clause throws an exception, that exception replaces any exception that was in the process of being thrown. If a finally clause issues a return statement, the method returns normally, even if an exception has been thrown and has not been handled yet.
try and finally can be used together without exceptions or any catch clauses. In this case, the finally block is simply cleanup code that is guaranteed to be executed, regardless of any break, continue, or return statements within the try clause.
In previous discussions of the for and continue statements, we've seen that a for loop cannot be naively translated into a while loop because the continue statement behaves slightly differently when used in a for loop than it does when used in a while loop. The finally clause gives us a way to write a while loop that is truly equivalent to a for loop. Consider the following generalized for loop:
for( initialize ; test ; increment ) statement
The following while loop behaves the same, even if the statement block contains a continue statement:
initialize ; while ( test ) { try { statement } finally { increment ; } }
Copyright © 2001 O'Reilly & Associates. All rights reserved.
This HTML Help has been published using the chm2web software. |