Exceptions in the Real World


So far, the point of this chapter has been to familiarize you with exceptions, and especially with trying, throwing, and catching. Now that you understand these concepts, it is time to tell you that the situation is actually a lot more complicated. There are many kinds of exceptions, each one indicating a different kind of problem, and each one capable of being handled separately.

The remainder of this chapter will show you how to deal with the multitude of real-world exceptions.

Two Families of Exceptions

The Exception class has more than 100 subclasses in the core Java packages. (The core packages are the ones that you get along with the JVM and the Java compiler. You can think of them as the infrastructure of Java, providing classes that are essential to the operation of the JVM, the compiler, and your own applications.)

The two families are

  • Checked exceptions

  • Runtime exceptions

The Exception class has a subclass called RuntimeException. The family of runtime exceptions consists of the RuntimeException class and all its subclasses. The family of checked exceptions consists of all other exception classes, including Exception itself. (Note that there is no CheckedException class.)

Generally, exception classes have long and descriptive names, such as PrinterIOException and ArrayIndexOutOfBoundsException. Usually, the class name tells you very specifically what went wrong. Let's use these two classes to look at the difference between checked and runtime exceptions.

PrinterIOException is a checked exception. It's thrown by methods that interact with a printer. If a printer is jammed, unavailable, or in some other failure state, the method throws PrinterIOException. ArrayIndexOutOfBoundsException is a runtime exception. As you can guess from the name, it is thrown when an array index is >= the length of the array, or when the index is negative.

What's the difference between these two situations? It all comes down to who is responsible for creating the problem. In the case of PrinterIOException, you can't really say it's anyone's fault. Printers jam up or fail in other ways that are familiar to all owners of printers. That's an environmental hazard. It's unavoidable, like bad weather. On the other hand, with ArrayIndexOutOfBoundsException, it's easy to assign blame. The programmer who wrote the line of code that used the illegitimate array index should have done a better job. After all, it would be ridiculous to tell you to turn to page 1,963 in this book… or worse yet, to page -47. Similarly, you shouldn't refer to an array element that doesn't exist.

To generalize from these examples: All checked exceptions represent situations that are unavoidable. All runtime exceptions represent situations that can be avoided by better programming. This implies that your Java programs might sometimes throw checked exceptions, but they should never throw runtime exceptions.

The proper way to deal with checked exceptions is with the try/catch mechanism described earlier in this chapter. The proper way to deal with runtime exceptions is… well, you should never have to deal with them, because your code should never throw them. Of course, code is never perfect the first time you write it. Whenever you write a long piece of code, your first job is getting it to compile. Once you do that, you're only halfway finished. The next step is to make your code run correctly by finding and eliminating bugs. During this phase of development, you are likely to encounter runtime exceptions, and your job is to eliminate them. So your finished, polished, ready-for-market code should never throw runtime exceptions. During development, runtime exceptions are signposts that point to code that needs fixing.

Runtime exceptions should not be caught in catch blocks. But how can this be? Earlier in this chapter, you learned that if code might throw an exception, the code has to appear in a try block and the exception has to be caught in a corresponding catch block. Well, that was an oversimplification to avoid giving you too much information all at once. Now that you're half an expert on exceptions, you can learn the whole story.

Code that throws checked exceptions must appear in a try block, with the exception caught in a catch block. But this rule does not apply to code that throws runtime exceptions. Such code may appear in a try/catch structure, but it doesn't have to, and usually it should not. Instead, the code that would throw the runtime exception should be fixed so that it no longer throws.

Runtime Exceptions and Stack Traces

Now you know that you should not catch runtime exceptions. But then what happens when one is thrown?

When any kind of exception is thrown, the JVM stores some very useful information in the exception. This information is called the stack trace, and often it's all you need to find the source of the problem. The stack trace tells you what line of code threw the exception, as well as the name of the method that contains the line. The stack trace also tells you what line of code called that method, and so on. It goes back and back until you get the line in your main() method that called the method that called the method that called the method that owned the line that threw the exception. It's like This Is the House That Jack Built, only it's about a Java program instead of a house:

This is the program that you built.

This is the ArrayIndexOutOfBoundsException that was thrown from the program that you built.

This is the line that threw the ArrayIndexOutOfBoundsException that was thrown from the program that you built.

This is the method that owns the line that threw the ArrayIndexOutOfBoundsException that was thrown from the program that you built.

This is the line that calls the method that owns the line that threw the ArrayIndexOutOfBoundsException that was thrown from the program that you built.

This is the method that owns the line that calls the method that owns the line that threw the ArrayIndexOutOfBoundsException that was thrown from the program that you built.

This is the main() method that owns the line that calls the method that owns the line that calls the method that owns the line… that threw the ArrayIndexOutOfBoundsException that was thrown from the program that you built.

Let's look at a practical example. Suppose you have the following application:

 1. public class ShowMeATrace  2. {  3.   public static void main(String[] args)  4.   {  5.     int[] cubes = new int[10];  6.     storeCubes(cubes);  7.   }  8.  9.   private static void storeCubes(int[] intArr) 10.   { 11.     for (int i=0; i<=10; i++) 12.       storeOneCube(intArr, i); 13.   } 14. 15.   private static void storeOneCube(int[] ints, 16.                                    int index) 17.   { 18.     ints[index] = index*index*index; 19.   } 20. }

The main() method creates an array that's passed to storeCubes(). The storeCubes() method loads each array component with the cube of its index. It does this by calling storeOneCube() once for each component. When you run this application, you get the following output:

java.lang.ArrayIndexOutOfBoundsException     at ShowMeATrace.storeOneCube(ShowMeATrace:18)     at ShowMeATrace.storeCubes(ShowMeATrace:12)     at ShowMeATrace.main(ShowMeATrace:6)     Exception in thread "main" 

This output is a stack trace. Reading from top to bottom, you find that an ArrayIndexOutOfBoundsException was thrown from line 18 in the storeOneCube() method. The offending call to storeOneCube() was made on line 12 in storeCubes(), which was called from line 6 in main(). (By the way, notice that the first line of the trace implies that ArrayIndexOutOfBoundsException belongs to the java.lang package. The core Java classes belong to a package called java, which contains many subpackages. The most important subpackage is java.lang, which contains a large number of vital infrastructure classes. You will look at some of these classes in the next chapter.)

So the stack trace tells you to pay attention to lines 18, 12, and 6. Usually your best strategy is to look at lines in the order they appear in the trace. Line 18 seems innocent, as long as index is reasonable. But index is supplied by the method's caller, so you look at line 12. You see that index in storeOneCube() corresponds to i in storeCubes(). The maximum value of i is 10, but the array only has 10 components, so the maximum legal index is 9. You have found the problem.

There are two ways to fix the bug. The lazy way would be to change line 11 like this:

for (int i=0; i<10; i++)

That would solve the problem at hand, but if the array size (in main()) ever changes, you will have to remember to change line 11. The safe way, which is better style in all cases, is to use the following for line 11:

for (int i=0; i<intArr.length; i++)

If you have a program that uses an array, it is very likely that eventually you will create a for loop to do some kind of processing on each array component. If you use the kind of for loop shown here, you will always be sure to process every component while avoiding ArrayIndexOutOfBoundsException.

Warning

Be aware that some versions of the JVM do not provide stack traces when exceptions are thrown. This usually happens because the JVM performs some kind of optimization that makes it impossible to piece together the stack trace information. When these machines throw an exception, you just get a message that tells you the class of the exception.

Checked Exceptions

In the previous section, you saw that you should not catch runtime exceptions, even though the language allows you to. However, when you call a method that throws a checked exception, you have no choice but to use the try/catch mechanism. If you don't, your code will not compile.

Suppose you have a method, called printRetAddr(), that prints your return address on an envelope. Assume you have the kind of printer that can detect whether it is loaded with paper or envelopes. If it is not loaded with envelopes, the method throws PrinterIOException, which is a checked exception. If you want to call the method, your code might do the following:

void printSomeEnvelopes(int nEnvelopes) {   for (int i=0; i<nEnvelopes; i++)     printRetAddr(); }

Simple enough, but it won't compile. Your compiler error will be something like this:

PrinterIOException must be caught or declared to be thrown at line xx, column xxx.

This tells you that you have two options. Your first option is to put the call to printRetAddr() inside a try block:

void printSomeEnvelopes(int nEnvelopes) {   for (int i=0; i<nEnvelopes; i++)   {     try     {       printRetAddr();     }     catch (PrinterIOException piox)     {       System.out.println("Please load printer " +                          "with envelopes.");     }   } }

Earlier in this chapter, you saw code that catches Exception. Here you see that any subclass of Exception may be caught (as long as it really is thrown in the try block; see Exercise 4 at the end of this chapter). The catch block will be executed if the try block causes a PrinterIOException to be thrown.

You have a second option. If you don't want to use try/catch, you can simply declare that printSomeEnvelopes() throws PrinterIOException:

void printSomeEnvelopes(int nEnvelopes)               throws PrinterIOException {   for (int i=0; i<nEnvelopes; i++)     printRetAddr(); }

Now any method that calls printSomeEnvelopes() must either use try/catch or declare that it too throws PrinterIOException.

Multiple Catch Blocks

Typically, code in a try block can throw more than one kind of exception. To illustrate this, let's look at another type of checked exception: ConnectException. This is usually thrown by code that attempts to connect to a machine on the network, such as a Web server. If the remote machine does not respond (because it has been turned off, or is undergoing maintenance, or has burned up), the code that detects the lack of response should throw a ConnectException. (You will look at network connections in detail in Chapter 13, "File Input and Output." For now, the point is that now you know about two checked exception types.)

To extend this example, let's make printSomeEnvelopes() more responsible. Suppose you have two utility methods at your disposal:

getNumEnvelopesInStock() Returns the number of envelopes left, not counting the ones you just printed. This value is retrieved from a remote database.

setNumEnvelopesInStock() Updates the number of envelopes left. This value is stored on the remote database.

Both methods throw ConnectException if the machine where the remote database resides cannot be contacted. Now printSomeEnvelopes() can be written like this:

void printSomeEnvelopes(int nEnvelopes) {   for (int i=0; i<nEnvelopes; i++)   {     try     {       printRetAddr();     }     catch (PrinterIOException piox)     {       System.out.println("Please load printer " +                          "with envelopes.");       return;     }   }   try   {     int nEnvelopesLeft = getNumEnvelopesInStock();     nEnvelopesLeft -= nEnvelopes;     setNumEnvelopesInStock(nEnvelopesLeft);   }   catch (ConnectException conx)   {     System.out.println("Couldn't connect.");   }   System.out.println("printSomeEnvelopes() done."); }

The second try block updates the remote database, taking into account the number of envelopes that were just printed. When printSomeEnvelopes() is called, there are four possibilities:

The code could run normally, with no exceptions being thrown. Neither catch block is executed. The method prints the "done" message and then returns.

A PrinterIOException is thrown from printRetAddr(). Execution jumps to the first catch block, which prints the "Please load…" message and then returns. (It returns because no envelopes were used, so the number in the database shouldn't be decremented. If the catch block did not return, the second try block would be executed.)

A ConnectException is thrown from getNumEnvelopesInStock(). Execution jumps to the second catch block, which prints the "Couldn't connect" message. Then execution continues after the catch block. The "done" message is printed, and then the method returns.

A ConnectException is thrown from setNumEnvelopesInStock(). Just as in the previous case, execution jumps to the second catch block, which prints the "Couldn't connect" message. Then the "done" message is printed and the method returns.

This code can be simplified. A single try block is allowed to throw multiple exception types, provided there is a catch block for each type. This might require multiple catch blocks for the try block:

void printSomeEnvelopes(int nEnvelopes) {   try   {     for (int i=0; i<nEnvelopes; i++)       printRetAddr();     int nEnvelopesLeft = getNumEnvelopesInStock();     nEnvelopesLeft -= nEnvelopes;     setNumEnvelopesInStock(nEnvelopesLeft);   }   catch (PrinterIOException piox)   {     System.out.println("Please load printer " +                        "with envelopes.");   }   catch (ConnectException cx)   {     System.out.println("Couldn't connect.");   }   System.out.println("printSomeEnvelopes() done."); }

The work has been consolidated into the single try block. There are two catch blocks. If the try block threw five or 50 exception types, there could be five or 50 catch blocks.

When the JVM detects a thrown exception in the try block, it scans the various catch blocks. The current pass through the try block is abandoned, and execution continues in the first catch block that is appropriate to the type of thrown exception. This version of the method behaves exactly like the previous version, but it's easier to read because all the normal execution code appears in the try block, while problems are handled in the various catch blocks. No matter how many catch blocks there are, a single thrown exception is only handled by one catch block. After the catch block runs (and it doesn't contain a return statement), execution continues after the last catch block.

Catch Blocks and instanceof

The previous section introduced multiple catch blocks. You learned that execution continues in the first catch block that is appropriate to the type of thrown exception. But what makes a catch block appropriate? You might think that the type declared in parentheses after catch must match the class of the exception that was thrown. But this is not the whole story. The whole story involves instanceof.

Recall from Chapter 10, "Interfaces", that the syntax for instanceof is

<reference> instanceof <type>

If the type is a class name, and the reference points to an object whose class is either the type or a subclass of the type, instanceof evaluates to true.

When the JVM looks for a catch block to handle an exception, it uses instanceof to determine whether or not a particular catch block is appropriate. To illustrate, let's blur the printSomeEnvelopes() example:

void printSomeEnvelopes(int nEnvelopes) {   try   {     // STUFF   }   catch (PrinterIOException piox)   {     // STUFF   }   catch (ConnectException cx)   {     // STUFF   }   System.out.println("printSomeEnvelopes() succeeded."); }

If the try block throws, the JVM asks if the exception is an instanceof PrinterIOException. If so, the first catch block is executed. Otherwise, the next catch block is tested. The JVM asks if the exception is an instanceof ConnectException. If so, the second catch block is executed. In either case, only the one catch block is executed; the other is ignored. If the executing catch block does not return, execution then proceeds at the first statement following the last catch block.

If no exception is thrown, the try block runs to completion and both catch blocks are skipped.

Sometimes you can take advantage of how the JVM determines the appropriate catch block. In the last revision of our example, the two different kinds of exceptions were handled differently, but this might not always be the case. Suppose you decide that no matter what kind of trouble crops up, printSomeEnvelopes() should just print a message that says "Could not print" and then return. If there is no trouble, the method should print "Succeeded."

Both PrinterIOException and ConnectException are subclasses of a common superclass called IOException. So printSomeEnvelopes() can be rewritten like this:

void printSomeEnvelopes(int nEnvelopes) {   try   {     for (int i=0; i<nEnvelopes; i++)       printRetAddr();     int nEnvelopesLeft = getNumEnvelopesInStock();     nEnvelopesLeft -= nEnvelopes;     setNumEnvelopesInStock(nEnvelopesLeft);     System.out.println("Succeeded");   }   catch (IOException iox)   {     System.out.println("Could not print");   } }

Now, any kind of exception that the try block might throw will pass the instanceof IOException test, so execution will end up in the single catch block.

You can get even more sophisticated. There is a kind of catch block that is informally called a safety net catch block. This is not official Java terminology, but it's very commonly used. You might have a try block that throws many subclasses of IOException, including PrinterIOException and ConnectException. Suppose those two types require individual handling, but all other types can be handled the same. You could do the following:

try {   // Lots   // and   // lots   // and   // lots   // of code that throws   // lots   // and   // lots   // and   // lots   // of subclasses of IOException } catch (PrinterIOException piox) {   // Special PrinterIOException handling } catch (ConnectException cx) {   // Special ConnectException handling } catch (IOException iox) {   // General IOException handling }

If either PrinterIOException or ConnectException is thrown, the appropriate specific catch block will be executed. If a different type of IOException is thrown, the JVM will first check if the exception is an instanceof PrinterIOException. It isn't, so next the JVM will check if it is an instanceof ConnectException. Again, it isn't, so the JVM checks if it is an instanceof IOException. And it is, because subclasses pass the instanceof test, so the last catch block is executed. You can see how the last catch block is a kind of safety net, catching all IOExceptions that aren't caught by the two specific catch blocks.

The safety net block could have caught Exception instead of IOException. The code would have identical behavior, but the safety net is overly general and is considered bad coding style. The exception type caught by a safety net should be the lowest-level subclass that gets the job done. See Exercise 5 at the end of this chapter to find out why.

When you use a safety net, be careful about the order of appearance of your catch blocks. Don't do the following:

try {   // Something } catch (IOException iox) {   // General IOException handling } catch (PrinterIOException piox) {   // Special PrinterIOException handling } catch (ConnectException cx) {   // Special ConnectException handling }

The second and third catch blocks can never be executed, because both PrinterIOException and ConnectException pass the instanceof IOException test. The compiler will not allow this code. You will get a compiler message that says something like, "Catch is unreachable at line xxx."

The Advanced Exception Lab animated illustration shows exception handling in situations where the try block can throw multiple exception types from any of several lines. To start the program, type java exceptions.AdvancedExceptionLab. You will see the display shown in Figure 11.3.

click to expand
Figure 11.3: Advanced Exception Lab

You get to choose the type of exception that will be thrown. Click on the Choose Type… button and you will see a dialog that lets you choose from four checked types, as shown in Figure 11.4.

click to expand
Figure 11.4: Choosing an exception type in Advanced Exception Lab

You can click on any of the exception types except the Exception superclass. You can also choose which line throws the exception by clicking on the checkbox on the line of your choice on the main screen. The lab lets you choose one of five code configurations (via the File?Configurations menu). In each configuration, a method called top() calls a method called middle(), which calls a method called bottom(). The bottom method has a try block from which an exception is thrown. Different configurations handle the exception differently. Figure 11.5 shows the Spread Around configuration, with an AWTException thrown from method ccc().

click to expand
Figure 11.5: Advanced Exception Lab reconfigured

Try all the configurations, and be sure that the exception handling makes sense to you in all cases.

Checked Exceptions and Stack Traces

You have already looked at stack traces in the context of runtime exceptions. Checked exceptions also have stack traces. However, you usually don't see the trace because you have to catch checked exceptions. If you want to see a stack trace from a checked exception, you can call the printStackTrace() method:

try {   getNumEnvelopesInStock(); } catch (ConnectException cx) {   System.out.println("Stress!");   cx.printStackTrace(); }

If an exception is thrown, this code will print the "Stress" message, followed by the exception's stack trace. This is extremely useful during development. However, before you ship your code to paying customers, you might want to delete the printStackTrace() calls. Paying customers might not want that much information.

Throwing Checked Exceptions

If you're writing a method that throws exceptions, you have to decide which exception type to throw. You have three options:

  • Throw Exception, as in the examples in the first half of this chapter.

  • Throw a subclass of Exception from the core Java classes.

  • Throw your own custom subclass.

The first option is not realistic. Your code will work, but throwing Exception doesn't tell anybody else who reads your code anything about the nature of the exceptional condition. Also, you may need to call your method in a try block that calls other methods that throw. If your method throws Exception, it may be difficult or impossible to write a decent set of catch blocks. This is especially true if the try block calls more than one method that throws Exception when it could have thrown a more specific type. Your code is always most robust when you throw exceptions that are as specific as possible.

Your second option is to throw a preexisting exception type chosen from the core Java classes. This is easy when you know how to explore the core Java packages and discover the names and behaviors of the many classes they provide. You will learn how to do this in the next chapter. For now, be aware that it's important to choose the most accurate and informative exception name you can find. Most existing types have very long and informative names.

Unfortunately, a lot of programmers always throw IOException, even when the problem has nothing to do with Input/Output. This is a bad habit. The rationale seems to be that, out of all the checked subclasses of Exception, IOException has the shortest name. Please don't yield to this temptation.

Once you decide on an exception type, you construct and throw just as you saw earlier in this chapter, when you constructed and threw Exception (which, as you now understand, you should never do). All exception subclasses in the core Java packages have two forms of constructors: a no-arguments version, and a version that takes a text message as an argument. It is always better to use the second form. Be sure to compose a message that is both accurate and helpful.

For example, earlier in this chapter you saw code that called a hypothetical method called getNumEnvelopesInStock(), which threw ConnectException. Put yourself in the shoes of the person who wrote that method. He might have done something like the following, assuming he had a method called connectionOK() that returned true if the connection to the database server was sound:

public int getNumEnvelopesInStock()            throws ConnectException {   if (connectionOK() == false)     throw new ConnectException();   // Get & return # of envelopes remaining.   . . . } 

However, it would be more informative to include a message in the exception. You might pass something like the following into the constructor: "Couldn't get # of envelopes from remote db." Then any catch block that caught your exception could call getMessage() on it, printing out the result if appropriate.

What should you do if there's no appropriately named exception subclass in the core packages? You have to fall back on your third option, which is to create your own class. To do this, first decide if you should create a checked exception or a runtime exception. In other words, does your exception represent an unavoidable hazard of existence, or is it a programming error that should be fixed? Most often you will create a checked exception. Next, choose a name. The name should end with Exception, because that's what other people expect. For example, you might decide that if the getNumEnvelopesInStock() method can't connect to its remote database, it should throw a custom exception type. A plausible name would be RemoteEnvelopeCountException. The name says that the class is definitely an exception, and that both remote access and the envelope count are involved.

Having chosen a name, next you have to decide on a superclass. A checked exception should extend Exception, IOException, or some other checked exception type. In general, extend IOException or one of its many subclasses if the exceptional condition you want to represent involves input or output. Otherwise, extend Exception. In the rare case when you want to create a runtime exception, extend RuntimeException. In this example, RemoteEnvelopeCountException will be a subclass of ConnecException, since the problem stems from an inability to connect to the remote machine that owns the database.

Your custom class does not need any data or methods. It will inherit everything it needs. All you have to do is create constructors. A custom exception should have both constructor versions. Here is the source for RemoteEnvelopeCountException, in its entirety:

import java.net.*; class RemoteEnvelopeCountException       extends ConnectException {   RemoteEnvelopeCountException() { }   RemoteEnvelopeCountException(String s)   {     super(s);   } }

The import line is required because the ConnectException superclass lives in the java.net package, which is one of the core Java packages that you'll see in the next chapter. The first constructor is a no-arguments constructor that seems to do nothing (but remember the chain of construction from Chapter 8, "Inheritance"). The second constructor takes a text message, which is passed to the superclass constructor.

Custom exceptions are thrown and caught just like standard types, so you could call getNumEnvelopesInStock() like this:

try {   int n = getNumEnvelopesInStock();   System.out.println(n + " envelopes remaining in stock."); } catch (RemoteEnvelopeCountException recx) {   System.out.println("Stress!");   System.out.println(recx.getMessage()); }

Generally, it's better to use an existing exception type if you can find one whose name accurately and helpfully describes the exceptional condition. However, if no such class exists, creating a custom class is good programming style.




Ground-Up Java
Ground-Up Java
ISBN: 0782141900
EAN: 2147483647
Year: 2005
Pages: 157
Authors: Philip Heller

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