Chapter 22

Exception Handling


CONTENTS


A sad reality associated with being human is that we are all error prone. Because software is a human creation, it often suffers from this same weakness. As nice as the Java programming language is, software written with it will inevitably contain errors. The Java architects realized this and built a complete error-handling mechanism into the Java language and runtime environment. Runtime errors in Java are collectively known as exceptions, and the act of detecting and dealing with these errors is known as exception handling. The focus of this chapter is on understanding exceptions and how to effectively deal with them in Java programs.

What Is Exception Handling?

Although most programs include a certain degree of error-handling overhead, few programmers have the time or resources to fully cover their tracks when it comes to handling every possible type of error that can occur in a running program. For example, is it really the responsibility of the programmer to detect when a user's hard drive fills up, or when the system runs out of memory? If so, what exactly should a program do when something like this occurs? Although Java doesn't completely provide an answer to the latter question, it does address the first one head on.

The Java programming environment borrows a very powerful error-handling technique from C++ known as exception handling. An exception is defined as an abnormal event that disrupts the normal flow of a program. Exception handling, therefore, is the process of detecting and responding to exceptions in a consistent and reliable manner. The term "exception" is used instead of "error" because exceptions represent exceptional, or abnormal, conditions that aren't necessarily errors. In this way, an exception is a very general concept, meaning that any number of abnormal events could be interpreted as an exception.

So, how do you decide what constitutes an abnormal enough condition to warrant an exception? Most of the time you don't-you let Java decide! The Java API defines practically every type of exception you'll ever need. For a detailed look at exactly what exceptions are defined by the Java API, refer to Chapters 27 through 35 in Part III, "Package, Class, and Interface Reference," which contain information about the different API packages and the exceptions defined in each.

Getting back to exception handling, Java provides a mechanism for detecting and handling exceptions as they occur. More specifically, any method that is at risk of causing an exceptional event is defined as potentially generating an exception. Generating an exception at runtime is also known as throwing an exception. Any code that calls this method is aware that the method can potentially throw a particular exception and can include special code to handle the exception should it be thrown. This process is known as catching an exception. In this way, you can think of an exception as an object that is thrown containing information about an abnormal event; the exception-handling code catches the exception object and uses any information it provides to help deal with the abnormal event. This actually describes the exact way exceptions work in Java.

To get a better understanding of the practical usage of exception handling, consider the exceptional condition of the system running out of memory. An exception is thrown indicating that the system is out of memory. The exception-handling code catches the exception and in return tries to run the Java garbage collector to free up memory.

Why Is Exception Handling Important?

Obviously, programmers have been dealing with errors for a long time, so you may be wondering why exceptions are such a big deal. This curiosity is reasonable, as there is a lot of code out there working fine without ever using exception-handling facilities. However, like many recent advances in software design, exception handling is meant more as a tool for improving the management of errors by providing a better approach to attacking the problem. The problem isn't so much that programmers don't know how to deal with errors or exceptional conditions; it's that every programmer has his or her own notion of how to go about doing it. Exception handling provides a very standardized approach to something that has been home-brewed for many years now: the handling of errors.

More specifically, exception handling provides the Java programmer with three distinct advantages over traditional error-handling techniques:

The first point is significant because, all too often, error-handling code is mixed in with code that actually performs a function. This may not seem like a big deal, but if you carry this approach into a huge project, complexities abound brought on by trying to differentiate between legitimate code that is necessary versus code that is trying to handle some obscure problem condition.

The second advantage of exception handling has to do with making errors accessible to other parts of a program. Typically, you only get a chance to deal with an error at the immediate point where the error occurs. Using exception handling, the error condition itself can be passed up the method call stack so that other objects get a chance to take action based on the problem.

Finally, the last major advantage to using exception handling is that of providing a clean way to organize and differentiate between different types of exceptional conditions. Because exceptions are implemented in Java as actual objects, they are open to the same design benefits of object-oriented programming, such as inheritance. You can define general types of exceptions that deal with a particular set of problems, and then define more specific types that are derived from the general one. In fact, the Java API makes great use of inheritance in the design of its standard exception classes.

Along with the organization benefit at the design stage, you also gain from being able to respond to exceptions based on their type. For example, you may want to handle a whole class of exceptions, such as exceptions relating to indexes being out of bounds. In this case, you wouldn't care if the culprit was a string index or an array index, because the general exception type handles both.

Types of Exceptions

In the discussion thus far, I've been using the terms "exception" and "error" pretty much interchangeably. Although they are the same in a general programming sense, Java does distinguish between the two. In Java, an exception is simply defined as an abnormal event that interrupts the normal flow of a program. Most programs are designed to handle any exceptions that may arise during the life of the program. An error is a specific type of exception that a program isn't expected to be able to handle. In other words, errors are a more serious strain of exceptions, which are a superset of errors. Check out Figure 22.1, which shows a simple diagram relating exceptions and errors.

Figure 22.1: The relationship between exceptions and errors.

Understanding that errors are a more serious type of exception doesn't really tell the whole story in regard to exceptions. Including errors, there are three different categories of exceptions common to Java programming:

Normal exceptions are exceptions that occur based on a specific piece of code. In other words, the location where a normal exception can occur is predictable, even if the exception itself is not. An example of a normal exception is a file not found exception, which occurs at a very specific location in a program when a file access is attempted but the file isn't found.

Unlike normal exceptions, runtime exceptions are exceptions whose location is much harder to nail down. For example, a null pointer exception can occur any time a null object is referenced, which basically includes any part of a Java program. Because this type of exception is much more general and hard to handle, it is considered a runtime exception. In actuality, all exceptions are thrown at runtime; it's just that runtime exceptions are a specialized type of exception that address problems in programs that have typically been considered runtime problems. Another example of a runtime exception is a divide by zero exception, which can occur in practically any mathematical calculation involving a division.

The third type of exception is errors, which are more serious exceptions that are aimed at handling catastrophic conditions. No, I don't mean catastrophic as in your monitor bursting into flames; I mean situations where it is very unlikely that a program could handle the problem. This brings up the big distinction between errors and the two prior types of exceptions: Java programs aren't expected to be able to handle errors. Errors are assumed to be beyond the control of a program and are used primarily within the Java system itself to deal with major problems such as running out of system memory or the runtime interpreter crashing.

Throwing Exceptions

You've already learned that the basic gist of exceptions is that an exception is thrown when an abnormal problem arises, and your program is responsible for catching the exception and somehow dealing with it. You'll learn the details of catching exceptions in the next section of this chapter. For now, let's focus on how exceptions are thrown to begin with.

Any code that is capable of throwing an exception must specifically be designed to do so. In other words, exceptions aren't just something that magically appear whenever a problem occurs; code that has the potential of causing problems must be designed to notify a program of these problems accordingly. The Java throws keyword provides the necessary mechanism to wire exception information into code that is potentially dangerous. The throws keyword is used at the method level, meaning that you declare a particular method capable of throwing a certain exception or set of exceptions, like this:

public void thisIsTrouble() throws anException {
  // method body
}

In this example, the throws keyword is used to specify that the thisIsTrouble method can generate an exception of type anException. Any code that calls this method knows immediately that the risk is there for the method to generate an anException exception. If you have a method that is capable of causing even more problems, you can define it as potentially throwing multiple exceptions, like this:

public void thisIsRealTrouble() throws anException, anotherException {
  // method body
}

In this example, the thisIsRealTrouble method can throw two different types of exceptions: anException and anotherException.

The whole point of throwing an exception is not to try to deal with a problem at the point where it occurs, but rather to try to allow other code to deal with the problem. In other words, a method that is declared as throwing a particular type of exception is notifying all code that calls it that it might have a problem that it needs help solving. The calling code is responsible for helping out the method by catching the exception and dealing with it as smoothly as possible. If the method itself were capable of handling the problem, it would do so quietly and never mention the problem to the calling code.

The Java programming language is pretty strict about handling exceptions. In fact, the Java compiler will generate warnings if you make calls to methods that throw exceptions without providing the necessary exception-handling code. In some cases, ignoring these warnings may be acceptable programming practice, but in general you should try to address exceptions that can be thrown at your code. Remember, whoever designed the method must have realized there was a potential for trouble, or they wouldn't have declared it as throwing an exception in the first place.

Catching Normal Exceptions

Let's look at exactly how the exception handling is carried out. You've already learned that exceptions are typically thrown relative to a particular method, so you should be able to isolate where an exception is coming from. The trick then is to look for an exception to occur right at the point where you call the method in question. Actually, the technique for handling exceptions involves both looking for the exception and providing code to deal with the exception. You carry out this technique using the try and catch keywords, like this:

try {
  thisIsTrouble();
}
catch(anException e) {
  System.out.println(e.getMessage());
}

This code makes a call to the thisIsTrouble method inside a try block. A try block is used to hold code that is at risk of throwing an exception. Any code executing in a try block is considered at risk of throwing an exception. If an exception occurs in the try block, the runtime system looks to the catch clause to see if the exception type matches the one that was thrown. If so, the code in the catch block is executed; this is the code that actually deals with, or handles, the exception. From this discussion, you may have guessed that there can be multiple catch clauses that respond to different types of exceptions. If so, you guessed right!

Take a look at the following code, which demonstrates how to handle multiple exceptions:

try {
  thisIsRealTrouble();
}
catch(anException e) {
  System.out.println(e.getMessage());
}
catch(anotherException e) {
  System.out.println(e.getMessage());
}

In this example, the thisIsRealTrouble method is called, which is capable of generating two different types of exceptions. Each of these exception types is handled in a separate catch clause, which provides a means to handle each type of exception in a unique manner.

As you just learned, the Java system matches up exception types with catch clauses when there is an exact match. However, it can also match exception types based on a superclass of an exception. It works like this: when an exception occurs in a try block, the catch clauses are examined by comparing the exception type to the type in each clause. If the exception type is either the same as the one in a catch clause or a subclass of the one in a catch clause, a match will occur. This allows you to sometimes provide multilevel catch handlers based on more derived exception types. If you happen to implement an exception-handling arrangement involving derived types, remember to list the more specific types first and the more general types last.

Keep in mind that the Java API defines a wide variety of exceptions that are used a great deal throughout the Java system. So, even though the discussion has been somewhat general thus far, remember that all the exception-handling techniques of trying and catching exceptions apply to standard Java exceptions as well. Check out the following code:

try {
  FileInputStream in = new FileInputStream("Data.txt");
}
  catch (FileNotFoundException e) {
  System.out.println("File not found : " + e.getMessage());
}

In this example, a file input stream is created using a constructor that takes a filename representing the file to read from. This constructor is defined as throwing a FileNotFoundException if the input file cannot be found. Because the constructor is capable of throwing this exception, the sample code creates the object within a try block and handles the exception FileNotFoundException in a catch block. Notice that this example code merely prints information about the exception to standard output. In a practical program, you would probably want to provide a more robust exception handler rather than just one that printed a message to standard output.

Catching Runtime Exceptions

So far you've focused your attention on handling exceptions that are generated by methods. If you recall from the earlier discussion on exception types, these exceptions are referred to as normal exceptions. Another strain of exception you learned about is runtime exceptions, which involve problems that aren't so easy to track down. The following code demonstrates a runtime exception and how it is handled:

try {
  int x = 100;
  for (int i = 10; i >= 0; i--)
    System.out.println(x / i);
}
catch(ArithmeticException e) {
  System.out.println(e.getMessage());
}

Although it is a little subtle, this code contains a pretty big problem that results in a runtime exception: the for loop results in a divide by zero exception in its last iteration. The exception is handled by encasing the problem calculation within a try block and providing a corresponding handler by means of a catch clause. If you hadn't handled this exception, the runtime system would have bailed out and exited the program with an error message. You provided a cleaner outlet by catching the exception yourself. Again, a more robust solution would probably be in order in a full-blown program, such as notifying the user of an infinite result.

The drawback to this whole approach of handling runtime exceptions is that it requires you to wrap any questionable code inside a try block, which can get pretty messy. The solution is to try to balance your handling of exceptions and only go after problems that are significant enough to really cause you trouble. Exceptions are only useful when used in moderation; they aren't meant as an easy out for every little problem you encounter.

Forced Execution

The structure of exception handling tends to cause parts of a program to conditionally execute based on an exception occurring. For this reason, there is a problem of code not getting called when it really should be, based on an exception occurring. For example, consider a program that needs to perform some type of cleanup, regardless of the outcome of a risky piece of code. The very nature of the try-catch technique of exception handling makes it difficult to ensure that a particular piece of code is always executed.

Enter the finally keyword. The finally keyword provides a means of executing a piece of code regardless of what happens within a try block. To use finally, you declare a finally block and place it after the try and catch blocks; any code inside the finally block is always executed no matter what happens in the try block. You use the finally keyword like this:

try {
  int x = 100;
  for (int i = 10; i >= 0; i--)
    System.out.println(x / i);
}
catch(ArithmeticException e) {
  System.out.println(e.getMessage());
}
finally {
  System.out.println("Can't get around me!");
}

In this example, no matter what happens in the try block, the code inside the finally block is always executed. It turns out that the try block generates an exception that results in the catch block being executed. Nevertheless, as soon as the catch block finishes, the finally block is executed, even if the try or catch blocks had code that resulted in a return from the method. Even though this example doesn't really demonstrate the finally keyword in complete context, the most common usage of it is to provide cleanup for objects that must be performed no matter what happens. Using finally, you can feel safe that a certain piece of code will always get called.

Please don't confuse the name or the usage of the finally keyword with the finalize method. Granted, in some ways they are used to perform a similar function, but finally is a keyword related to exception handling, whereas finalize is a specific method related to object de- struction.

When to Use Exception Handling

Even though exception handling is a very powerful and useful facility of the Java programming and runtime environment, it doesn't aim to solve all problems relating to abnormal program execution. You will no doubt encounter many situations where an exception could be in order but where it may be more simple to just handle the problem with an if statement. This thinking doesn't apply to exceptions that are already defined in Java; you can already count on them being important enough to worry about.

The point here is that exception handling should be reserved for legitimate problems where you don't mind taking more time to define a problem and build in the necessary overhead to deal with it. Notice the usage of the term "overhead," which implies that exceptions carry with them a certain amount of baggage. Exceptions do in fact cost more to implement and work with, which is an issue you must weigh when you start thinking about exceptionizing your code.

My suggestion is to handle all the standard Java exceptions accordingly and then assess the needs of your own code carefully. Studying the use of exceptions in the Java API is a very good way to learn about when and where to use exceptions. The Java architects made a lot of tradeoffs in their usage of exceptions, which is a good indicator of how much of a judgment issue exception handling is. Ultimately, experience will be your guide as you work with exceptions in practical programs.

The best approach when designing your own code is to look at how a potential problem impacts the code around it. In other words, does the problem exist only within the confines of a particular method, or can it cause larger problems to the program at large? If the problem can be reasonably dealt with locally, there is probably no need to worry about defining or throwing an exception. However, if there is a chance that the problem can reach beyond the immediate method or object, then by all means toss that exception so other code will know about the risks.

The only other issue to consider is how often the risky code will potentially get called. Because exception handling involves more overhead than most other types of error handling, you don't want to have a bunch of exception-handling code in the middle of a routine that needs to be very efficient, such as a loop that performs some repetitive function. One solution to this problem is to throw an exception the first time a problem occurs, rather than over and over in a piece of code that gets called a lot.

Summary

In this chapter, you learned about imperfections in programming. More importantly, you learned about how Java eases the pain and allows us to be imperfect and still develop robust programs through the use of exception handling. You learned what exception handling is and why it is important in Java development. You then covered the basic kinds of exceptions, which include normal exceptions, runtime exceptions, and errors. You then progressed to how exceptions are thrown and caught, along with how to force the execution of code regardless of exceptional conditions. You finished up the chapter with a practical look at how exceptions can be leveraged alongside more traditional error-handling techniques.