Section 4.5. Exceptions


4.5. Exceptions

Java has its roots in embedded systemssoftware that runs inside specialized devices, such as hand-held computers, cellular phones, and fancy toasters. In those kinds of applications, it's especially important that software errors be handled robustly. Most users would agree that it's unacceptable for their phone to simply crash or for their toast (and perhaps their house) to burn because their software failed. Given that we can't eliminate the possibility of software errors, it's a step in the right direction to recognize and deal with anticipated application-level errors methodically.

Dealing with errors in a language such as C is entirely the responsibility of the programmer. The language itself provides no help in identifying error types and no tools for dealing with them easily. In C, a routine generally indicates a failure by returning an "unreasonable" value (e.g., the idiomatic -1 or null). As the programmer, you must know what constitutes a bad result and what it means. It's often awkward to work around the limitations of passing error values in the normal path of data flow. [*] An even worse problem is that certain types of errors can legitimately occur almost anywhere, and it's prohibitive and unreasonable to explicitly test for them at every point in the software.

[*] The somewhat obscure setjmp( ) and longjmp( ) statements in C can save a point in the execution of code and later return to it unconditionally from a deeply buried location. In a limited sense, this is the functionality of exceptions in Java.

Java offers an elegant solution to these problems through exceptions. (Java exception handling is similar to, but not quite the same as, exception handling in C++.) An exception indicates an unusual condition or an error condition. Program control becomes unconditionally transferred or "thrown" to a specially designated section of code where it's caught and handled. In this way, error handling is orthogonal to (or independent of) the normal flow of the program. We don't have to have special return values for all our methods; errors are handled by a separate mechanism. Control can be passed a long distance from a deeply nested routine and handled in a single location when that is desirable, or an error can be handled immediately at its source. A few standard methods return -1 as a special value, but these are generally limited to situations where we are expecting a special value and the situation is not really out of bounds.[*]

[*] For example, the getHeight( ) method of the Image class returns -1 if the height isn't known yet. No error has occurred; the height will be available in the future. In this situation, throwing an exception would be inappropriate.

A Java method is required to specify the exceptions it can throw (i.e., the ones that it doesn't catch itself), and the compiler makes sure that users of the method handle them. In this way, the information about what errors a method can produce is promoted to the same level of importance as its argument and return types. You may still decide to punt and ignore obvious errors, but in Java you must do so explicitly.

4.5.1. Exceptions and Error Classes

Exceptions are represented by instances of the class java.lang.Exception and its subclasses. Subclasses of Exception can hold specialized information (and possibly behavior) for different kinds of exceptional conditions. However, more often they are simply "logical" subclasses that serve only to identify a new exception type. Figure 4-1 shows the subclasses of Exception in the java.lang package. It should give you a feel for how exceptions are organized. Most other packages define their own exception types, which usually are subclasses of Exception itself or of its important subclass RuntimeException, which we'll discuss in a moment.

For example, an important exception class is IOException in the package java.io. The IOException class extends Exception and has many subclasses for typical I/O problems (such as a FileNotFoundException) and networking problems (such as a MalformedURLException). Network exceptions belong to the java.net package. Another important descendant of IOException is RemoteException, which belongs to the java.rmi package. It is used when problems arise during remote method

Figure 4-1. The java.lang.Exception subclasses


invocation (RMI). Throughout this book, we mention exceptions you need to be aware of as we encounter them.

An Exception object is created by the code at the point where the error condition arises. It can be designed to hold whatever information is necessary to describe the exceptional condition and also includes a full stack trace for debugging. (A stack trace is the list of all the methods called in order to reach the point where the exception was thrown.) The Exception object is passed as an argument to the handling block of code, along with the flow of control. This is where the terms "throw" and "catch" come from: the Exception object is thrown from one point in the code and caught by the other, where execution resumes.

The Java API also defines the java.lang.Error class for unrecoverable errors. The subclasses of Error in the java.lang package are shown in Figure 4-2. A notable Error type is AssertionError, which is used by the assert statement to indicate a failure (assertions are discussed later in this chapter). A few other packages define their own subclasses of Error, but subclasses of Error are much less common (and less useful) than subclasses of Exception. You generally needn't worry about these errors in your code (i.e., you do not have to catch them); they are intended to indicate fatal problems or virtual machine errors. An error of this kind usually causes the Java interpreter to display a message and exit. You are actively discouraged from trying to catch or recover from them because they are supposed to indicate a fatal program bug, not a routine condition.

Figure 4-2. The java.lang.Error subclasses


Both Exception and Error are subclasses of THRowable. The THRowable class is the base class for objects which can be "thrown" with the throw statement. In general, you should extend only Exception, Error, or one of their subclasses.

4.5.2. Exception Handling

The try/catch guarding statements wrap a block of code and catch designated types of exceptions that occur within it:

     try {         readFromFile("foo");         ...     }     catch ( Exception e ) {         // Handle error         System.out.println( "Exception while reading file: " + e );         ...     }

In this example, exceptions that occur within the body of the try portion of the statement are directed to the catch clause for possible handling. The catch clause acts like a method; it specifies as an argument the type of exception it wants to handle and, if it's invoked, it receives the Exception object as an argument. Here, we receive the object in the variable e and print it along with a message.

A try statement can have multiple catch clauses that specify different types (subclasses) of Exception:

     try {         readFromFile("foo");         ...     }     catch ( FileNotFoundException e ) {         // Handle file not found         ...     }     catch ( IOException e ) {         // Handle read error         ...     }     catch ( Exception e ) {         // Handle all other errors         ...     }

The catch clauses are evaluated in order, and the first assignable match is taken. At most, one catch clause is executed, which means that the exceptions should be listed from most to least specific. In the previous example, we anticipate that the hypothetical readFromFile( ) can throw two different kinds of exceptions: one for a file not found and another for a more general read error. Any subclass of Exception is assignable to the parent type Exception, so the third catch clause acts like the default clause in a switch statement and handles any remaining possibilities. We've shown it here for completeness, but in general you want to be as specific as possible in the exception types you catch.

One beauty of the try/catch scheme is that any statement in the try block can assume that all previous statements in the block succeeded. A problem won't arise suddenly because a programmer forgot to check the return value from some method. If an earlier statement fails, execution jumps immediately to the catch clause; later statements are never executed.

4.5.3. Bubbling Up

What if we hadn't caught the exception? Where would it have gone? Well, if there is no enclosing TRy/catch statement, the exception pops to the top of the method in which it appeared and is, in turn, thrown from that method up to its caller. If that point in the calling method is within a TRy clause, control passes to the corresponding catch clause. Otherwise, the exception continues propagating up the call stack, from one method to its caller. In this way, the exception bubbles up until it's caught, or until it pops out of the top of the program, terminating it with a runtime error message. There's a bit more to it than that because, in this case, the compiler might have reminded us to deal with it, but we'll get back to that in a moment.

Let's look at another example. In Figure 4-3, the method getContent( ) invokes the method openConnection( ) from within a try/catch statement. In turn, openConnection( ) invokes the method sendRequest( ), which calls the method write( ) to send some data.

Figure 4-3. Exception propagation


In this figure, the second call to write( ) throws an IOException. Since sendRequest( ) doesn't contain a try/catch statement to handle the exception, it's thrown again from the point where it was called in the method openConnection( ). Since openConnection( ) doesn't catch the exception either, it's thrown once more. Finally, it's caught by the try statement in getContent( ) and handled by its catch clause.

4.5.4. Stack Traces

Since an exception can bubble up quite a distance before it is caught and handled, we may need a way to determine exactly where it was thrown. It's also very important to know the context of how the point of the exception was reached; that is, which methods called which methods to get to that point. All exceptions can dump a stack trace that lists their method of origin and all the nested method calls it took to arrive there. Most commonly, the user sees a stack trace when it is printed using the printStackTrace( ) method.

     try {         // complex, deeply nested task     } catch ( Exception e ) {         // dump information about exactly where the exception occurred         e.printStackTrace( System.err );         ...     }

For example, the stack trace for an exception might look like this:

     java.io.FileNotFoundException: myfile.xml           at java.io.FileInputStream.<init>(FileInputStream.java)           at java.io.FileInputStream.<init>(FileInputStream.java)           at MyApplication.loadFile(MyApplication.java:137)           at MyApplication.main(MyApplication.java:5)

This stack trace indicates that the main( ) method of the class MyApplication called the method loadFile( ). The loadFile( ) method then tried to construct a FileInputStream, which threw the FileNotFoundException. Note that once the stack trace reaches Java system classes (like FileInputStream), the line numbers may be lost. This can also happen when the code is optimized by some virtual machines. Usually, there is a way to disable the optimization temporarily to find the exact line numbers. However, in tricky situations, changing the timing of the application can affect the problem you're trying to debug.

Prior to Java 1.4, stack traces were limited to a text output that was really suitable for printing and reading only by humans. Now methods allow you to retrieve the stack trace information programmatically, using the Throwable getStackTrace( ) method. This method returns an array of StackTraceElement objects, each of which represents a method call on the stack. You can ask a StackTraceElement for details about that method's location using the methods getFileName( ), getClassName( ), getMethodName( ), and getLineNumber( ). Element zero of the array is the top of the stack, the final line of code that caused the exception; subsequent elements step back one method call each until the original main( ) method is reached.

4.5.5. Checked and Unchecked Exceptions

We mentioned earlier that Java forces us to be explicit about our error handling, but it's not realistic to require that every conceivable type of error be handled in every situation. Java exceptions are therefore divided into two categories: checked and unchecked. Most application-level exceptions are checked, which means that any method that throws one, either by generating it itself (as we'll discuss later) or by ignoring one that occurs within it, must declare that it can throw that type of exception in a special tHRows clause in its method declaration. We haven't yet talked in detail about declaring methods (see Chapter 5). For now all you need to know is that methods have to declare the checked exceptions they can throw or allow to be thrown.

Again in Figure 4-3, notice that the methods openConnection( ) and sendRequest( ) both specify that they can throw an IOException. If we had to throw multiple types of exceptions, we could declare them separated with commas:

     void readFile( String s ) throws IOException, InterruptedException {         ...     }

The tHRows clause tells the compiler that a method is a possible source of that type of checked exception and that anyone calling that method must be prepared to deal with it. The caller may use a try/catch block to catch it, or, in turn, it may declare that it can throw the exception itself.

In contrast, exceptions that are subclasses of either the class java.lang. RuntimeException or the class java.lang.Error are unchecked. See Figure 4-1 for the subclasses of RuntimeException. (Subclasses of Error are generally reserved for serious class loading or runtime system problems.) It's not a compile-time error to ignore the possibility of these exceptions; methods also don't have to declare they can throw them. In all other respects, unchecked exceptions behave the same as other exceptions. We are free to catch them if we wish, but in this case we aren't required to.

Checked exceptions are intended to cover application-level problems, such as missing files and unavailable hosts. As good programmers (and upstanding citizens), we should design software to recover gracefully from these kinds of conditions. Unchecked exceptions are intended for system-level problems, such as "out of memory" and "array index out of bounds." While these may indicate application-level programming errors, they can occur almost anywhere and usually aren't possible to recover from. Fortunately, because they are unchecked exceptions, you don't have to wrap every one of your array-index operations in a try/catch statement.

To sum up, checked exceptions are problems a reasonable application should try to handle gracefully; unchecked exceptions (runtime exceptions or errors) are problems from which we would not normally expect our software to recover. Error types are those explicitly intended to be conditions that we should not normally try to handle or recover from.

4.5.6. Throwing Exceptions

We can throw our own exceptions, either instances of Exception, one of its existing subclasses, or our own specialized exception classes. All we have to do is create an instance of the Exception and throw it with the throw statement:

     throw new IOException(  );

Execution stops and is transferred to the nearest enclosing try/catch statement that can handle the exception type. (There is little point in keeping a reference to the Exception object we've created here.) An alternative constructor lets us specify a string with an error message:

     throw new IOException("Sunspots!");

You can retrieve this string by using the Exception object's getMessage( ) method. Often, though, you can just print (or toString( )) the exception object itself to get the message and stack trace.

By convention, all types of Exception have a String constructor like this. The earlier String message is somewhat facetious and vague. Normally, you won't throw a plain old Exception but a more specific subclass. Here's another example:

     public void checkRead( String s ) {         if ( new File(s).isAbsolute(  ) || (s.indexOf("..") != -1) )             throw new SecurityException(                "Access to file : "+ s +" denied.");     }

In this code, we partially implement a method to check for an illegal path. If we find one, we throw a SecurityException, with some information about the transgression.

Of course, we could include whatever other information is useful in our own specialized subclasses of Exception. Often, though, just having a new type of exception is good enough because it's sufficient to help direct the flow of control. For example, if we are building a parser, we might want to make our own kind of exception to indicate a particular kind of failure:

     class ParseException extends Exception {         ParseException(  ) {             super(  );         }         ParseException( String desc ) {             super( desc );         }     }

See Chapter 5 for a full description of classes and class constructors. The body of our Exception class here simply allows a ParseException to be created in the conventional ways we've created exceptions previously (either generically or with a simple string description). Now that we have our new exception type, we can guard like this:

     // Somewhere in our code     ...     try {         parseStream( input );     } catch ( ParseException pe ) {         // Bad input...     } catch ( IOException ioe ) {         // Low-level communications problem     }

As you can see, although our new exception doesn't currently hold any specialized information about the problem (it certainly could), it does let us distinguish a parse error from an arbitrary I/O error in the same chunk of code.

4.5.6.1 Chaining exceptions

Sometimes you'll want to take some action based on an exception and then turn around and throw a new exception in its place. This is common when building frameworks, where low-level detailed exceptions are handled and represented by higher-level exceptions that can be managed more easily. For example, you might want to catch an IOException in a communication package, possibly perform some cleanup, and ultimately throw a higher-level exception of your own, maybe something like LostServerConnection.

You can do this in the obvious way by simply catching the exception and then throwing a new one, but then you lose important information, including the stack trace of the original "causal" exception. To deal with this, you can use the technique of exception chaining. This means that you include the causal exception in the new exception that you throw. Java has explicit support for exception chaining. The base Exception class can be constructed with an exception as an argument or the standard String message and an exception:

     throw new Exception( "Here's the story...", causalException );

You can get access to the wrapped exception later with the getCause( ) method. More importantly, Java automatically prints both exceptions and their respective stack traces if you print the exception or if it is shown to the user.

You can add this kind of constructor to your own exception subclasses (delegating to the parent constructor). However, since this API is a recent addition to Java (added in Version 1.4), many existing exception types do not provide this kind of constructor. You can still take advantage of this pattern by using the Throwable method initCause( ) to set the causal exception explicitly after constructing your exception and before throwing it:

     try {       // ...     } catch ( IOException cause ) {       Exception e =         new IOException("What we have here is a failure to communicate...");       e.initCause( cause );       throw e;     }

4.5.7. try Creep

The try statement imposes a condition on the statements that it guards. It says that if an exception occurs within it, the remaining statements are abandoned. This has consequences for local variable initialization. If the compiler can't determine whether a local variable assignment placed inside a try/catch block will happen, it won't let us use the variable. For example:

     void myMethod(  ) {         int foo;         try {             foo = getResults(  );         }         catch ( Exception e ) {             ...         }         int bar = foo;  // Compile-time error -- foo may not have been initialized

In this example, we can't use foo in the indicated place because there's a chance it was never assigned a value. One obvious option is to move the assignment inside the TRy statement:

     try {         foo = getResults(  );         int bar = foo;  // Okay because we get here only                         // if previous assignment succeeds     }     catch ( Exception e ) {         ...     }

Sometimes this works just fine. However, now we have the same problem if we want to use bar later in myMethod( ). If we're not careful, we might end up pulling everything into the try statement. The situation changes, however, if we transfer control out of the method in the catch clause:

     try {         foo = getResults(  );     }     catch ( Exception e ) {         ...         return;     }     int bar = foo;  // Okay because we get here only                     // if previous assignment succeeds

The compiler is smart enough to know that if an error had occurred in the try clause, we wouldn't have reached the bar assignment, so it allows us to refer to foo. Your code will dictate its own needs; you should just be aware of the options.

4.5.8. The finally Clause

What if we have some cleanup to do before we exit our method from one of the catch clauses? To avoid duplicating the code in each catch branch and to make the cleanup more explicit, use the finally clause. A finally clause can be added after a try and any associated catch clauses. Any statements in the body of the finally clause are guaranteed to be executed, no matter how control leaves the try body (whether an exception was thrown or not):

     try {         // Do something here     }     catch ( FileNotFoundException e ) {         ...     }     catch ( IOException e ) {         ...     }     catch ( Exception e ) {         ...     }     finally {         // Cleanup here is always executed     }

In this example, the statements at the cleanup point are executed eventually, no matter how control leaves the try. If control transfers to one of the catch clauses, the statements in finally are executed after the catch completes. If none of the catch clauses handles the exception, the finally statements are executed before the exception propagates to the next level.

If the statements in the try execute cleanly, or if we perform a return, break, or continue, the statements in the finally clause are still executed. To perform cleanup operations, we can even use try and finally without any catch clauses:

     try {         // Do something here         return;     }     finally {         System.out.println("Whoo-hoo!");     }

Exceptions that occur in a catch or finally clause are handled normally; the search for an enclosing try/catch begins outside the offending try statement, after the finally has been executed.

4.5.9. Performance Issues

Because of the way the Java virtual machine is implemented, guarding against an exception being thrown (using a try) is free. It doesn't add any overhead to the execution of your code. However, throwing an exception is not free. When an exception is thrown, Java has to locate the appropriate try/catch block and perform other time-consuming activities at runtime.

The result is that you should throw exceptions only in truly "exceptional" circumstances and avoid using them for expected conditions, especially when performance is an issue. For example, if you have a loop, it may be better to perform a small test on each pass and avoid throwing the exception rather than throwing it frequently. On the other hand, if the exception is thrown only once in a gazillion times, you may want to eliminate the overhead of the test code and not worry about the cost of throwing that exception. The general rule should be that exceptions are used for "out of bounds" or abnormal situations, not routine and expected conditions (such as the end of a file).



    Learning Java
    Learning Java
    ISBN: 0596008732
    EAN: 2147483647
    Year: 2005
    Pages: 262

    flylib.com © 2008-2017.
    If you may any questions please contact us: flylib@qtcs.net