2.5. StatementsA statement is a single command 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-5 summarizes the statements defined by Java.
2.5.1. Expression StatementsAs 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; they 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 2.5.2. Compound StatementsA 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. 2.5.3. The Empty StatementAn 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 in a for loop: for(int i = 0; i < 10; a[i++]++) // Increment array elements /* empty */; // Loop body is empty statement 2.5.4. Labeled StatementsA labeled statement is simply a statement that has been given a name by prepending an 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 } } 2.5.5. Local Variable Declaration StatementsA local variable, often simply called a variable, is a symbolic name for a location to store a value 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 local 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 method definition int i = 0; // Declare variable i while (i < 10) { // i is in scope here int j = 0; // Declare j; the scope of j begins here i++; // i is in scope here; increment it } // 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 2.5.6. The if/else StatementThe 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 the interpreter skips the statement. In Java 5.0, the expression may be of the wrapper type Boolean instead of the primitive type boolean. In this case, the wrapper object is automatically unboxed. Here is an example if statement: if (username == null) // If username is null, username = "John Doe"; // use a default value 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 like this: 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"); } 2.5.6.1 The else if clauseThe 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 else if 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 else if 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 } } } 2.5.7. The switch StatementAn 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 starts with an expression whose type is an int, short, char, or byte. In Java 5.0 Integer, Short, Character and Byte wrapper types are allowed, as are enumerated types. (Enums are new in Java 5.0; see Chapter 4 for details on enumerated types and their use in switch statements.) This expression is followed by a block of code in curly braces that contains various entry points that correspond to possible values for the expression. 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"); } } The switch statement and its case labels have some important restrictions. 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 expression 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. 2.5.8. The while StatementJust 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, which must result in a boolean (or, in Java 5.0, a Boolean) value. If the value 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 loop counters, although you should use more descriptive names if it makes your code easier to understand. 2.5.9. The do StatementA 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 ) ; Notice a couple of differences 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 just discussed: int count = 0; do { System.out.println(count); count++; } while(count < 10); The do loop is much less commonly used than its while cousin because, in practice, it is unusual to encounter a situation where you are sure you always want a loop to execute at least once. 2.5.10. The for StatementThe 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 ; update) statement This for loop is basically equivalent to the following while loop:[2]
initialize; while(test) { statement; update; } Placing the initialize, test, and update 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 update the loop variable. The interpreter discards the values of the initialize and update expressions, so in order to be useful, these expressions must have side effects. initialize is typically an assignment expression while update 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 update 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 update expressions of a for loop can use a comma to separate multiple initializations and update 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 update 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(;;). 2.5.11. The for/in StatementThe for/in statement is a powerful new loop that was added to the language in Java 5.0. It iterates through the elements of an array or collection or any object that implements java.lang.Iterable (we'll see more about this new interface in a moment). On each iteration it assigns an element of the array or Iterable object to the loop variable you declare and then executes the loop body, which typically uses the loop variable to operate on the element. No loop counter or Iterator object is involved; the for/in loop performs the iteration automatically, and you need not concern yourself with correct initialization or termination of the loop. A for/in loop is written as the keyword for followed by an open parenthesis, a variable declaration (without initializer), a colon, an expression, a close parenthesis, and finally the statement (or block) that forms the body of the loop. for( declaration : expression ) statement Despite its name, the for/in loop does not use the keyword in. It is common to read the colon as "in," however. Because this statement does not have a keyword of its own, it does not have an unambiguous name. You may also see it called " enhanced for" or "foreach." For the while, do, and for loops, we've shown an example that prints ten numbers. The for/in loop can do this too, but not on its own. for/in is not a general-purpose loop like the others. It is a specialized loop that executes its body once for each element in an array or collection. So, in order to loop ten times (to print out ten numbers), we need an array or other collection with ten elements. Here's code we can use: // These are the numbers we want to print int[] primes = new int[] { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 }; // This is the loop that prints them for(int n : primes) System.out.println(n); Here are some more things you should know about the syntax of the for/in loop:
The following class further illustrates the use of the for/in statement. It relies on parameterized types, which are covered in Chapter 4, and you may want to return to this section after reading that chapter. import java.util.*; public class ForInDemo { public static void main(String[] args) { // This is a collection we'll iterate over below. Set<String> wordset = new HashSet<String>( ); // We start with a basic loop over the elements of an array. // The body of the loop is executed once for each element of args[]. // Each time through one element is assigned to the variable word. for(String word : args) { System.out.print(word + " "); wordset.add(word); } System.out.println( ); // Now iterate through the elements of the Set. for(String word : wordset) System.out.print(word + " "); } } 2.5.11.1 Iterable and iteratorTo understand how the for/in loop works with collections, we need to consider two interfaces, java.lang.Iterable , introduced in Java 5.0, and java.util.Iterator, introduced in Java 1.2, but parameterized with the rest of the Collections Framework in Java 5.0.[3] The APIs of both interfaces are reproduced here for convenience:
public interface Iterator<E> { boolean hasNext( ); E next( ); void remove( ); } Iterator defines a way to iterate through the elements of a collection or other data structure. It works like this: while there are more elements in the collection (hasNext( ) returns true), call next( ) to obtain the next element of the collection. Ordered collections, such as lists, typically have iterators that guarantee that they'll return elements in order. Unordered collections like Set simply guarantee that repeated calls to next( ) return all elements of the set without omissions or duplications but do not specify an ordering. public interface Iterable<E> { java.util.Iterator<E> iterator( ); } The Iterable interface was introduced to make the for/in loop work. A class implements this interface in order to advertise that it is able to provide an Iterator to anyone interested. (This can be useful in its own right, even when you are not using the for/in loop). If an object is Iterable<E>, that means that that it has an iterator( ) method that returns an Iterator<E>, which has a next( ) method that returns an object of type E. If you implement Iterable and provide an Iterator for your own classes, you'll be able to iterate over those classes with the for/in loop. Remember that if you use the for/in loop with an Iterable<E>, the loop variable must be of type E or a superclass or interface. For example, to iterate through the elements of a List<String>, the variable must be declared String or its superclass Object, or one of its interfaces CharSequence, Comparable, or Serializable. If you use for/in to iterate through the elements of a raw List with no type parameter, the Iterable and Iterator also have no type parameter, and the type returned by the next( ) method of the raw Iterator is Object. In this case, you have no choice but to declare the loop variable to be an Object. 2.5.11.2 What for/in cannot dofor/in is a specialized loop that can simplify your code and reduce the possibility of looping errors in many circumstances. It is not a general replacement for the while, for, or do loops, however, because it hides the loop counter or Iterator from you. This means that some algorithms simply cannot be expressed with a for/in loop. Suppose you want to print the elements of an array as a comma-separated list. To do this, you need to print a comma after every element of the array except the last, or equivalently, before every element of the array except the first. With a traditional for loop, the code might look like this: for(int i = 0; i < words.length; i++) { if (i > 0) System.out.print(", "); System.out.print(words[i]); } This is a very straightforward task, but you simply cannot do it with for/in. The problem is that the for/in loop doesn't give you a loop counter or any other way to tell if you're on the first iteration, the last iteration, or somewhere in between. Here are two other simple loops that can't be converted to use for/in, for the same basic reason: String[] args; // Initialized elsewhere for(int i = 0; i < args.length; i++) System.out.println(i + ": " + args[i]); // Map words to the position at which they occur. List<String> words; // Initialized elsewhere Map<String,Integer> map = new HashMap<String,Integer>( ); for(int i = 0, n = words.size( ); i < n; i++) map.put(words.get(i), i); A similar issue exists when using for/in to iterate through the elements of the collection. Just as a for/in loop over an array has no way to obtain the array index of the current element, a for/in loop over a collection has no way to obtain the Iterator object that is being used to itemize the elements of the collection. This means, for example, that you cannot use the remove( ) method of the iterator (or any of the additional methods defined by java.util.ListIterator) as you could if you used the Iterator explicitly yourself. Here are some other things you cannot do with for/in:
2.5.12. The break StatementA 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 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 2.5.13. The continue StatementWhile 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:
2.5.14. The return StatementA 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 that they do not return any value. The Java interpreter runs methods like this by executing their 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 2.5.15. The synchronized StatementJava makes it easy to write multithreaded programs (see Chapter 5 for examples). When working with multiple threads, 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.) 2.5.16. The throw StatementAn 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 itto 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. 2.5.16.1 Exception typesAn exception in Java is an object. The type of this object is java.lang.Throwable, or more commonly, some subclass[4] of Throwable that more specifically describes the type of exception that occurred. 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 exceptions 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.
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. 2.5.17. The try/catch/finally StatementThe 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. } 2.5.17.1 tryThe 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. 2.5.17.2 catchA 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. 2.5.17.3 finallyThe finally clause is generally used to clean up after the code in the TRy clause (e.g., close files and 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 yet been handled. 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 handles the continue statement in the same way that a for loop does. Consider the following generalized for loop: for( initialize ; test ; update ) statement The following while loop behaves the same, even if the statement block contains a continue statement: initialize ; while ( test ) { TRy { statement } finally { update ; } } Note, however, that placing the update statement within a finally block causes this while loop to respond to break statements differently than the for loop does. 2.5.18. The assert StatementAn assert statement is used to document and verify design assumptions in Java code. This statement was added in Java 1.4 and cannot be used with previous versions of the language. An assertion consists of the assert keyword followed by a boolean expression that the programmer believes should always evaluate to TRue. By default, assertions are not enabled, and the assert statement does not actually do anything. It is possible to enable assertions as a debugging and testing tool, however; when this is done, the assert statement evaluates the expression. If it is indeed TRue, assert does nothing. On the other hand, if the expression evaluates to false, the assertion fails, and the assert statement throws a java.lang.AssertionError. The assert statement may include an optional second expression, separated from the first by a colon. When assertions are enabled and the first expression evaluates to false, the value of the second expression is taken as an error code or error message and is passed to the AssertionError( ) constructor. The full syntax of the statement is: assert assertion ; or: assert assertion : errorcode ; It is important to remember that the assertion must be a boolean expression, which typically means that it contains a comparison operator or invokes a boolean-valued method. 2.5.18.1 Compiling assertionsBecause the assert statement was added in Java 1.4, and because assert was not a reserved word prior to Java 1.4, the introduction of this new statement can cause code that uses "assert" as an identifier to break. For this reason, the javac compiler does not recognize the assert statement by default. To compile Java code that uses the assert statement, you must use the command-line argument -source 1.4. For example: javac -source 1.4 ClassWithAssertions.java In Java 1.4, the javac compiler allows "assert" to be used as an identifier unless -source 1.4 is specified. If it finds assert used as an identifier, it issues an incompatibility warning to encourage you to modify your code. In Java 5.0, the javac compiler recognizes the assert statement (as well as all the new Java 5.0 syntax) by default, and no special compiler arguments are required to compile code that contains assertions. If you have legacy code that still uses assert as an identifier, it will no longer compile by default in Java 5.0. If you can't fix it, you can compile it in Java 5.0 using the -source 1.3 option. 2.5.18.2 Enabling assertionsassert statements encode assumptions that should always be true. For efficiency, it does not make sense to test assertions each time code is executed. Thus, by default, assertions are disabled, and assert statements have no effect. The assertion code remains compiled in the class files, however, so it can always be enabled for testing, diagnostic, and debugging purposes. You can enable assertions, either across the board or selectively, with command-line arguments to the Java interpreter. To enable assertions in all classes except for system classes, use the -ea argument. To enable assertions in system classes, use -esa. To enable assertions within a specific class, use -ea followed by a colon and the classname: java -ea:com.example.sorters.MergeSort com.example.sorters.Test To enable assertions for all classes in a package and in all of its subpackages, follow the -ea argument with a colon, the package name, and three dots: java -ea:com.example.sorters... com.example.sorters.Test You can disable assertions in the same way, using the -da argument. For example, to enable assertions throughout a package and then disable them in a specific class or subpackage, use: java -ea:com.example.sorters... -da:com.example.sorters.QuickSort java -ea:com.example.sorters... -da:com.example.sorters.plugins... If you prefer verbose command-line arguments, you can use -enableassertions and -disableassertions instead of -ea and -da and -enablesystemassertions instead of -esa. Java 1.4 added to java.lang.ClassLoader methods for enabling and disabling the assertions for classes loaded through that ClassLoader. If you use a custom class loader in your program and want to turn on assertions, you may be interested in these methods. See ClassLoader in the reference section. 2.5.18.3 Using assertionsBecause assertions are disabled by default and impose no performance penalty on your code, you can use them liberally to document any assumptions you make while programming. It may take some time to get used to this, but as you do, you'll find more and more uses for the assert statement. Suppose, for example, that you're writing a method in such a way that you know that the variable x is either 0 or 1. Without assertions, you might code an if statement that looks like this: if (x = = 0) { ... } else { // x is 1 ... } The comment in this code is an informal assertion indicating that you believe that within the body of the else clause, x will always equal 1. Now suppose your code is later modified in such a way that x can take on a value other than 0 and 1. The comment and the assumption that go along with it are no longer valid, and this may cause a bug that is not immediately apparent or is difficult to localize. The solution in this situation is to convert your comment into an assert statement. The code becomes: if (x = = 0) { ... } else { assert x = = 1 : x // x must be 0 or 1 ... } Now, if x somehow ends up holding an unexpected value, an AssertionError is thrown, which makes the bug immediately apparent and easy to pinpoint. Furthermore, the second expression (following the colon) in the assert statement includes the unexpected value of x as the "error message" of the AssertionError. This message is not intended to mean anything to an end user, but to provide enough information so that you know not just that an assertion failed but also what caused it to fail. A similar technique is useful with switch statements. If you write a switch statement without a default clause, you make an assumption about the set of possible values for the switch expression. If you believe that no other value is possible, you can add an assert statement to document and validate that fact. For example: switch(x) { case -1: return LESS; case 0: return EQUALS; case 1: return GREATER; default: assert false:x; // Throw AssertionError if x is not -1, 0, or 1. } Note that the form assert false; always fails. It is a useful "dead-end" statement when you believe that the statement can never be reached. Another common use of the assert statement is to test whether the arguments passed to a method all have values that are legal for that method; this is also known as enforcing method preconditions. For example: private static Object[] subArray(Object[] a, int x, int y) { assert x <= y : "subArray: x > y"; // Precondition: x must be <= y // Now go on to create and return a subarray of a... } Note that this is a private method. The programmer has used an assert statement to document a precondition of the subArray( ) method and state that she believes that all methods that invoke this private method do in fact honor that precondition. She can state this because she has control over all the methods that invoke subArray( ). She can verify her belief by enabling assertions while testing the code. But once the code is tested, if assertions are left disabled, the method does not suffer the overhead of testing its arguments each time it is called. Note that the programmer did not use an assert statement to test that argument a is non-null and that the x and y arguments were legal indexes into that array. These implicit preconditions are always tested by Java at runtime, and a failure results in an unchecked NullPointerException or an ArrayIndexOutOfBoundsException, so an assertion is not required for them. It is important to understand that the assert statement is not suitable for enforcing preconditions on public methods. A public method can be called from anywhere, and the programmer cannot assert in advance that it will be invoked correctly. To be robust, a public API must explicitly test its arguments and enforce its preconditions each time it is called, whether or not assertions are enabled. A related use of the assert statement is to verify a class invariant. Suppose you are creating a class that represents a list of objects and allows objects to be inserted and deleted but always maintains the list in sorted order. You believe that your implementation is correct and that the insertion methods always leave the list in sorted order, but you want to test this to be sure. You might write a method that tests whether the list is actually sorted, then use an assert statement to invoke the method at the end of each method that modifies the list. For example: public void insert(Object o) { ... // Do the insertion here assert isSorted( ); // Assert the class invariant here } When writing code that must be threadsafe, you must obtain locks (using a synchronized method or statement) when required. One common use of the assert statement in this situation is to verify that the current thread holds the lock it requires: assert Thread.holdsLock(data); The Thread.holdsLock( ) method was added in Java 1.4 primarily for use with the assert statement. To use assertions effectively, you must be aware of a couple of fine points. First, remember that your programs will sometimes run with assertions enabled and sometimes with assertions disabled. This means that you should be careful not to write assertion expressions that contain side effects. If you do, your code will run differently when assertions are enabled than it will when they are disabled. There are a few exceptions to this rule, of course. For example, if a method contains two assert statements, the first can include a side effect that affects only the second assertion. Another use of side effects in assertions is the following idiom that determines whether assertions are enabled (which is not something that your code should ever really need to do): boolean assertions = false; // Whether assertions are enabled assert assertions = true; // This assert never fails but has a side effect Note that the expression in the assert statement is an assignment, not a comparison. The value of an assignment expression is always the value assigned, so this expression always evaluates to TRue, and the assertion never fails. Because this assignment expression is part of an assert statement, the assertions variable is set to true only if assertions are enabled. In addition to avoiding side effects in your assertions, another rule for working with the assert statement is that you should never try to catch an AssertionError (unless you catch it at the top level simply so that you can display the error in a more user-friendly fashion). If an AssertionError is thrown, it indicates that one of the programmer's assumptions has not held up. This means that the code is being used outside of the parameters for which it was designed, and it cannot be expected to work correctly. In short, there is no plausible way to recover from an AssertionError, and you should not attempt to catch it. |