Java ScreenShot
Screenshot      

Screenshot Core Java 2: Volume I - Fundamentals

Table of Contents
 11.  Exceptions and Debugging


Dealing with Errors

Suppose an error occurs while a Java program is running. The error might be caused by a file containing wrong information, a flaky network connection, or (we hate to mention it) use of an invalid array index or an attempt to use an object reference that hasn't yet been assigned to an object. Users expect that programs will act sensibly when errors happen. If an operation cannot be completed because of an error, the program ought to either:

  • Return to a safe state and enable the user to execute other commands;

or

  • Allow the user to save all his or her work and terminate the program gracefully.

This may not be easy to do, since the code that detects (or even causes) the error condition is usually far removed from the code that can roll back the data to a safe state, or the code that can save the user's work and exit cheerfully. The mission of exception handling is to transfer control from where the error occurred to an error-handler that can deal with the situation. To handle exceptional situations in your program, you must take into account the errors and problems that may occur. What sorts of problems do you need to consider? User input errors. In addition to the inevitable typos, some users like to blaze their own trail instead of following directions. Suppose, for example, that a user asks to connect to a URL that is syntactically wrong. Your code should check the syntax, but suppose it does not. Then the network package will complain. Device errors. Hardware does not always do what you want it to. The printer may be turned off. A web page may be temporarily unavailable. Devices will often fail in the middle of a task. For example, a printer may run out of paper in the middle of a printout. Physical limitations. Disks can fill up; you can run out of available memory. Code errors. A method may not perform correctly. For example, it could deliver wrong answers or use other methods incorrectly. Computing an invalid array index, trying to find a nonexistent entry in a hash table, and trying to pop an empty stack are all examples of a code error. The traditional reaction to an error in a method is to return a special error code that the calling method analyzes. For example, methods that read information back from files often return a –1 end-of-file value marker rather than a standard character. This can be an efficient method for dealing with many exceptional conditions. Another common return value to denote an error condition is the null reference. In , you saw an example of this with the getParameter method of the Applet class that returns null if the queried parameter is not present. Unfortunately, it is not always possible to return an error code. There may be no obvious way of distinguishing valid and invalid data. A method returning an integer cannot simply return –1 to denote the error—the value –1 might be a perfectly valid result. Instead, as we mentioned back in , Java allows every method an alternate exit path if it is unable to complete its task in the normal way. In this situation, the method does not return a value. Instead, it throws an object that encapsulates the error information. Note that the method exits immediately; it does not return its normal (or any) value. Moreover, execution does not resume at the code that called the method; instead, the exception-handling mechanism begins its search for an exception handler that can deal with this particular error condition. Exceptions have their own syntax and are part of a special inheritance hierarchy. We take up the syntax first and then give a few hints on how to use this language feature effectively.

The Classification of Exceptions

In Java, an exception object is always an instance of a class derived from Throwable. As you will soon see, you can create your own exception classes, if the ones built into Java do not suit your needs. Screenshot-1 is a simplified diagram of the exception hierarchy in Java.

Screenshot-1. Exception hierarchy in Java

Java graphics 11fig01.gif


Notice that all exceptions descend from Throwable, but the hierarchy immediately splits into two branches: Error and Exception. The Error hierarchy describes internal errors and resource exhaustion inside the Java runtime system. You should not throw an object of this type. There is little you can do if such an internal error occurs, beyond notifying the user and trying to terminate the program gracefully. These situations are quite rare. When doing Java programming, you focus on the Exception hierarchy. The Exception hierarchy also splits into two branches: exceptions that derive from RuntimeException and those that do not. The general rule is this:

  • A RuntimeException happens because you made a coding error. Any other exception occurs because a bad thing, such as an I/O error, happened to your otherwise good program.

Exceptions that inherit from RuntimeException include such problems as:

  • A bad cast;
  • An out-of-bounds array access;
  • A null pointer access.

Exceptions that do not inherit from RuntimeException include:

  • Trying to read past the end of a file;
  • Trying to open a malformed URL;
  • Trying to find a Class object for a string that does not denote an existing class.

The rule "If it is a RuntimeException, it was your fault" works pretty well. You could have avoided that ArrayIndexOutOfBoundsException by testing the array index against the array bounds. The NullPointerException would not have happened had you checked whether or not the variable was null before using it. How about a malformed URL? Isn't it also possible to find out whether it is "malformed" before using it? Well, different browsers can handle different kinds of URLs. For example, Netscape can deal with a mailto: URL, whereas the applet viewer cannot. Thus, the notion of "malformed" depends on the environment, not just on your code. The Java Language Specification calls any exception that derives from the class Error or the class RuntimeException an unchecked exception. All other exceptions are called checked exceptions. This is useful terminology that we will also adopt.

Java graphics notes_icon.gif

The name RuntimeException is somewhat confusing. Of course, all of the errors we are discussing occur at run time.

Java graphics cplus_icon.gif

If you are familiar with the (much more limited) exception hierarchy of the standard C++ library, you will be really confused at this point. C++ has two fundamental exception classes, runtime_error and logic_error. The logic_error class is the equivalent of Java's RuntimeException and also denotes logical errors in the program. The runtime_error class is the base class for exceptions caused by unpredictable problems. It is equivalent to exceptions in Java that are not of type RuntimeException.

Advertising the Exceptions That a Method Throws

A Java method can throw an exception if it encounters a situation it cannot handle. The idea is simple: a method will not only tell the Java compiler what values it can return, it is also going to tell the compiler what can go wrong. For example, code that attempts to read from a file knows that the file might not exist or that it might be empty. The code that tries to process the information in a file therefore will need to notify the compiler that it can throw some sort of IOException. The place where you advertise that your method can throw an exception is in the header of the method; the header changes to reflect the checked exceptions the method can throw. For example, here is the header for a method in the BufferedReader class from the standard library. The method reads a line of text from a stream, such as a file or network connection. (See for more on streams.)

public String readLine() throws IOException


The header indicates this method returns a string, but it also has the capacity to go wrong in a special way—by throwing an IOException. If this sad state should come to pass, the method will not return a string but instead will throw an object of the IOException class. If it does, then the runtime system will begin to search for an exception handler that knows how to deal with IOException objects. When you write your own methods, you don't have to advertise every possible throwable object that your method might actually throw. To understand when (and what) you have to advertise in the throws clause of the methods you write, keep in mind that an exception is thrown in any of the following four situations:

  1. You call a method that throws a checked exception, for example, the readLine method of the BufferedReader class.

  2. You detect an error and throw a checked exception with the throw statement (we cover the throw statement in the next section).

  3. You make a coding error, such as a[-1] = 0 that gives rise to an unchecked exception such as an ArrayIndexOutOfBoundsException.

  4. An internal error occurs in the virtual machine or runtime library.

If either of the first two scenarios occurs, you must tell the programmers who will use your method that there is the possibility of an exception. Why? Any method that throws an exception is a potential death trap. If no handler catches the exception, the current thread of execution terminates. As with Java methods that are part of the supplied classes, you declare that your method may throw an exception with an exception specification in the method header.

class MyAnimation
{
 . . .
 public Image loadImage(String s) throws IOException
 {
 . . .
 }
}


If a method might throw more than one checked exception, you must indicate all exceptions in the header. Separate them by a comma as in the following example:

class MyAnimation
{
 . . .
 public Image loadImage(String s)
 throws EOFException, MalformedURLException
 {
 . . .
 }
}


However, you do not need to advertise internal Java errors, that is, exceptions inheriting from Error. Any code could potentially throw those exceptions, and they are entirely beyond your control. Similarly, you should not advertise unchecked exceptions inheriting from RuntimeException.

class MyAnimation
{
 . . .
 void drawImage(int i)
 throws ArrayIndexOutOfBoundsException // NO!!!
 {
 . . .
 }
}


These runtime errors are completely under your control. If you are so concerned about array index errors, you should spend the time needed to fix them instead of advertising the possibility that they can happen. In summary, a method must declare all the checked exceptions that it might throw. Unchecked exceptions are either beyond your control (Error) or result from conditions that you should not have allowed in the first place (RuntimeException). If your method fails to faithfully declare all checked exceptions, the compiler will issue an error message. Of course, as you have already seen in quite a few examples, instead of declaring the exception, you can also catch it. Then the exception won't be thrown out of the method, and no throws specification is necessary. You will see later in this chapter how to decide whether to catch an exception or to enable someone else to catch it.

Java graphics caution_icon.gif

If you override a method from a superclass in your subclass, the subclass method cannot throw more checked exceptions than the superclass method that you replace. (It can throw fewer, if it likes.) In particular, if the superclass method throws no checked exception at all, neither can the subclass. For example, if you override JComponent.paintComponent, your paintComponent method must not throw any checked exceptions, since the superclass method doesn't throw any.

When a method in a class declares that it throws an exception that is an instance of a particular class, then it may throw an exception of that class or of any of its subclasses. For example, the readLine method of the BufferedReader class says that it throws an IOException. We do not know what kind of IOException. It could be a plain IOException or an object of one of the various child classes, such as EOFException.
Java graphics cplus_icon.gif

The throws specifier is the same as the throw specifier in C++, with one important difference. In C++, throw specifiers are enforced at run time, not at compile time. That is, the C++ compiler pays no attention to exception specifications. But if an exception is thrown in a function that is not part of the throw list, then the unexpected function is called, and, by default, the program terminates. Also, in C++, a function may throw any exception if no throw specification is given. In Java, a method without a throws specifier may not throw any checked exception at all.

How to Throw an Exception

Let us suppose something terrible has happened in your code. You have a method, readData, that is reading in a file whose header promised

Content-length: 1024


But, you get an end of file after 733 characters. You decide this situation is so abnormal that you want to throw an exception. You need to decide what exception type to throw. Some kind of IOException would be a good choice. Perusing the Java API documentation, you find an EOFException with the description "Signals that an EOF has been reached unexpectedly during input." Perfect. Here is how you throw it:

throw new EOFException();


or, if you prefer,

EOFException e = new EOFException();
throw e;


Here is how it all fits together:

String readData(BufferedReader in) throws EOFException
{
 . . .
 while (. . .)
 {
 if (ch == -1) // EOF encountered
 {
 if (n < len)
 throw new EOFException();
 }
 . . .
 }
 return s;
}


The EOFException has a second constructor that takes a string argument. You can put this to good use by describing the exceptional condition more carefully.

String gripe = "Content-length: " + len + ", Received: " + n;
throw new EOFException(gripe);


As you can see, throwing an exception is easy if one of the existing exception classes works for you. In this case:

  1. Find an appropriate exception class;

  2. Make an object of that class;

  3. Throw it.

Once a method throws an exception, the method does not return to its caller. This means that you do not have to worry about cooking up a default return value or an error code.

Java graphics cplus_icon.gif

Throwing an exception is the same in C++ and in Java, with one small exception. In Java, you can throw only objects of child classes of Throwable. In C++, you can throw values of any type.

Creating Exception Classes

Your code may run into a problem that is not adequately described by any of the standard exception classes. In this case, it is easy enough to create your own exception class. Just derive it from Exception or from a child class of Exception such as IOException. It is customary to give both a default constructor and a constructor that contains a detailed message. (The toString method of the Throwable base class prints out that detailed message, which is handy for debugging.)

class FileFormatException extends IOException
{
 public FileFormatException() {}
 public FileFormatException(String gripe)
 {
 super(gripe);
 }
}


Now you are ready to throw your very own exception type.

String readData(BufferedReader in) throws FileFormatException
{
 . . .
 while (. . .)
 {
 if (ch == -1) // EOF encountered
 {
 if (n < len)
 throw new FileFormatException();
 }
 . . .
 }
 return s;
}


java.lang.Throwable 1.0

Java graphics api_icon.gif
  • Throwable()

    constructs a new Throwable object with no detailed message.

  • Throwable(String message)

    constructs a new Throwable object with the specified detailed message. By convention, all derived exception classes support both a default constructor and a constructor with a detailed message.

  • String getMessage()

    gets the detailed message of the Throwable object.


Java ScreenShot
Screenshot      
Top
 

Comments