Thinking in Java, 3rd ed. Revision 4.0


prcdentsommairesuivant

9. Error Handling with Exceptions

The basic philosophy of Java is that "badly formed code will not be run."

The ideal time to catch an error is at compile time, before you even try to run the program. However, not all errors can be detected at compile time. The rest of the problems must be handled at run time through some formality that allows the originator of the error to pass appropriate information to a recipient who will know how to handle the difficulty properly.

C and other earlier languages often had multiple error-handling schemes, and these were generally established by convention and not as part of the programming language. Typically, you returned a special value or set a flag, and the recipient was supposed to look at the value or the flag and determine that something was amiss. However, as the years passed, it was discovered that programmers who use a library tend to think of themselves as invincible-as in, "Yes, errors might happen to others, but not in my code." So, not too surprisingly, they wouldn't check for the error conditions (and sometimes the error conditions were too silly to check for. (40) If you were thorough enough to check for an error every time you called a method, your code could turn into an unreadable nightmare. Because programmers could still coax systems out of these languages, they were resistant to admitting the truth: that this approach to handling errors was a major limitation to creating large, robust, maintainable programs.

The solution is to take the casual nature out of error handling and to enforce formality. This actually has a long history, because implementations of exception handling go back to operating systems in the 1960s, and even to BASIC's "on error goto." But C++ exception handling was based on Ada, and Java's is based primarily on C++ (although it looks more like that in Object Pascal).

The word "exception" is meant in the sense of "I take exception to that." At the point where the problem occurs, you might not know what to do with it, but you do know that you can't just continue on merrily; you must stop, and somebody, somewhere, must figure out what to do. But you don't have enough information in the current context to fix the problem. So you hand the problem out to a higher context where someone is qualified to make the proper decision (much like a chain of command).

The other rather significant benefit of exceptions is that they clean up error handling code. Instead of checking for a particular error and dealing with it at multiple places in your program, you no longer need to check at the point of the method call (since the exception will guarantee that someone catches it). And, you need to handle the problem in only one place, the so-called exception handler. This saves you code, and it separates the code that describes what you want to do from the code that is executed when things go awry. In general, reading, writing, and debugging code becomes much clearer with exceptions than when using the old way of error handling.

Because exception handling is the only official way that Java reports errors, and it is enforced by the Java compiler, there are only so many examples that can be written in this book without learning about exception handling. This chapter introduces you to the code you need to write to properly handle exceptions, and the way you can generate your own exceptions if one of your methods gets into trouble.

9.1. Basic exceptions

An exceptional condition is a problem that prevents the continuation of the method or scope that you're in. It's important to distinguish an exceptional condition from a normal problem, in which you have enough information in the current context to somehow cope with the difficulty. With an exceptional condition, you cannot continue processing because you don't have the information necessary to deal with the problem in the current context. All you can do is jump out of the current context and relegate that problem to a higher context. This is what happens when you throw an exception.

Division is a simple example. If you're about to divide by zero, it's worth checking for that condition. But what does it mean that the denominator is zero? Maybe you know, in the context of the problem you're trying to solve in that particular method, how to deal with a zero denominator. But if it's an unexpected value, you can't deal with it and so must throw an exception rather than continuing along that execution path.

When you throw an exception, several things happen. First, the exception object is created in the same way that any Java object is created: on the heap, with new. Then the current path of execution (the one you couldn't continue) is stopped and the reference for the exception object is ejected from the current context. At this point the exception handling mechanism takes over and begins to look for an appropriate place to continue executing the program. This appropriate place is the exception handler, whose job is to recover from the problem so the program can either try another tack or just continue.

As a simple example of throwing an exception, consider an object reference called t. It's possible that you might be passed a reference that hasn't been initialized, so you might want to check before trying to call a method using that object reference. You can send information about the error into a larger context by creating an object representing your information and "throwing" it out of your current context. This is called throwing an exception. Here's what it looks like:

 
Sélectionnez

if(t == null)
	throw new NullPointerException();


This throws the exception, which allows you-in the current context-to abdicate responsibility for thinking about the issue further. It's just magically handled somewhere else. Precisely where will be shown shortly.

9.1.1. Exception arguments

Like any object in Java, you always create exceptions on the heap using new, which allocates storage and calls a constructor. There are two constructors in all standard exceptions; The first is the default constructor, and the second takes a string argument so you can place pertinent information in the exception:

 
Sélectionnez

throw new NullPointerException("t = null");


This string can later be extracted using various methods, as you'll see.

The keyword throw causes a number of relatively magical things to happen. Typically, you'll first use new to createan object that represents the error condition. You give the resulting reference to throw. The object is, in effect, "returned" from the method, even though that object type isn't normally what the method is designed to return. A simplistic way to think about exception handling is as a different kind of return mechanism, although you get into trouble if you take that analogy too far. You can also exit from ordinary scopes by throwing an exception. But a value is returned, and the method or scope exits.

Any similarity to an ordinary return from a method ends here, because where you return is someplace completely different from where you return for a normal method call. (You end up in an appropriate exception handler that might be far-many levels away on the call stack-from where the exception was thrown.)

In addition, you can throw any type of Throwable (the exception root class) object that you want. Typically, you'll throw a different class of exception for each different type of error. The information about the error is represented both inside the exception object and implicitly in the name of the exception class, so someone in the bigger context can figure out what to do with your exception. (Often, the only information is the type of exception, and nothing meaningful is stored within the exception object.)

9.2. Catching an exception

If a method throws an exception, it must assume that exception will be "caught" and dealt with. One of the advantages of exception handling is that it allows you to concentrate on the problem you're trying to solve in one place, and then deal with the errors from that code in another place.

To see how an exception is caught, you must first understand the concept of a guarded region. This is a section of code that might produce exceptions and is followed by the code to handle those exceptions.

9.2.1. The try block

If you're inside a method and you throw an exception (or another method you call within this method throws an exception), that method will exit in the process of throwing. If you don't want a throw to exit the method, you can set up a special block within that method to capture the exception. This is called the try block because you "try" your various method calls there. The try block is an ordinary scope preceded by the keyword try:

 
Sélectionnez

try {
	// Code that might generate exceptions
}


If you were checking for errors carefully in a programming language that didn't support exception handling, you'd have to surround every method call with setup and error testing code, even if you call the same method several times. With exception handling, you put everything in a try block and capture all the exceptions in one place. This means your code is much easier to write and read because the goal of the code is not confused with the error checking.

9.2.2. Exception handlers

Of course, the thrown exception must end up someplace. This "place" is the exception handler, and there's one for every exception type you want to catch. Exception handlers immediately follow the try block and are denoted by the keyword catch:

 
Sélectionnez

try {
	// Code that might generate exceptions
} catch(Type1 id1) {
	// Handle exceptions of Type1
} catch(Type2 id2) {
	// Handle exceptions of Type2
} catch(Type3 id3) {
	// Handle exceptions of Type3
}
 
// etc...


Each catch clause (exception handler) is like a little method that takes one and only one argument of a particular type. The identifier (id1, id2, and so on) can be used inside the handler, just like a method argument. Sometimes you never use the identifier because the type of the exception gives you enough information to deal with the exception, but the identifier must still be there.

The handlers must appear directly after the try block. If an exception is thrown, the exception handling mechanism goes hunting for the first handler with an argument that matches the type of the exception. Then it enters that catch clause, and the exception is considered handled. The search for handlers stops once the catch clause is finished. Only the matching catch clause executes; it's not like a switch statement in which you need a break after each case to prevent the remaining ones from executing.

Note that within the try block, a number of different method calls might generate the same exception, but you need only one handler.

9.2.2.1. Termination vs. resumption

There are two basic models in exception handling theory. In termination (which is what Java and C++ support), you assume that the error is so critical that there's no way to get back to where the exception occurred. Whoever threw the exception decided that there was no way to salvage the situation, and they don't want to come back.

The alternative is called resumption. It means that the exception handler is expected to do something to rectify the situation, and then the faulting method is retried, presuming success the second time. If you want resumption, it means you still hope to continue execution after the exception is handled. In this case, your exception is more like a method call-which is how you should set up situations in Java in which you want resumption-like behavior. (That is, don't throw an exception; call a method that fixes the problem.) Alternatively, place your try block inside a while loop that keeps reentering the try block until the result is satisfactory.

Historically, programmers using operating systems that supported resumptive exception handling eventually ended up using termination-like code and skipping resumption. So although resumption sounds attractive at first, it isn't quite so useful in practice. The dominant reason is probably the coupling that results; your handler must often be aware of where the exception is thrown, and contain nongeneric code specific to the throwing location. This makes the code difficult to write and maintain, especially for large systems where the exception can be generated from many points.

9.3. Creating your own exceptions

You're not stuck using the existing Java exceptions. The JDK exception hierarchy can't foresee all the errors you might want to report, so you can create your own to denote a special problem that your library might encounter.

To create your own exception class, you must inherit from an existing exception class, preferably one that is close in meaning to your new exception (although this is often not possible). The most trivial way to create a new type of exception is just to let the compiler create the default constructor for you, so it requires almost no code at all:

 
Sélectionnez

//: c09:SimpleExceptionDemo.java
// Inheriting your own exceptions.
import com.bruceeckel.simpletest.*;
 
class SimpleException extends Exception {}
 
public class SimpleExceptionDemo {
	private static Test monitor = new Test();
	public void f() throws SimpleException {
		System.out.println("Throw SimpleException from f()");
		throw new SimpleException();
	}
	public static void main(String[] args) {
		SimpleExceptionDemo sed = new SimpleExceptionDemo();
		try {
			sed.f();
		} catch(SimpleException e) {
			System.err.println("Caught it!");
		}
		monitor.expect(new String[] {
			"Throw SimpleException from f()",
			"Caught it!"
		});
	}
} ///:~


The compiler creates a default constructor, which automatically (and invisibly) calls the base-class default constructor. Of course, in this case you don't get a SimpleException(String) constructor, but in practice that isn't used much. As you'll see, the most important thing about an exception is the class name, so most of the time an exception like the one shown here is satisfactory.

Here, the result is printed to the console standard error stream by writing to System.err. This is usually a better place to send error information than System.out, which may be redirected. If you send output to System.err, it will not be redirected along with System.out so the user is more likely to notice it.

You can also create an exception class that has a constructor with a String argument:

 
Sélectionnez

//: c09:FullConstructors.java
import com.bruceeckel.simpletest.*;
 
class MyException extends Exception {
	public MyException() {}
	public MyException(String msg) { super(msg); }
}
 
public class FullConstructors {
	private static Test monitor = new Test();
	public static void f() throws MyException {
		System.out.println("Throwing MyException from f()");
		throw new MyException();
	}
	public static void g() throws MyException {
		System.out.println("Throwing MyException from g()");
		throw new MyException("Originated in g()");
	}
	public static void main(String[] args) {
		try {
			f();
		} catch(MyException e) {
			e.printStackTrace();
		}
		try {
			g();
		} catch(MyException e) {
			e.printStackTrace();
		}
		monitor.expect(new String[] {
			"Throwing MyException from f()",
			"MyException",
			"%% \tat FullConstructors.f\\(.*\\)",
			"%% \tat FullConstructors.main\\(.*\\)",
			"Throwing MyException from g()",
			"MyException: Originated in g()",
			"%% \tat FullConstructors.g\\(.*\\)",
			"%% \tat FullConstructors.main\\(.*\\)"
		});
	}
} ///:~


The added code is small: two constructors that define the way MyException is created. In the second constructor, the base-class constructor with a String argument is explicitly invoked by using the super keyword.

In the handlers, one of the Throwable (from which Exception is inherited) methods is called: printStackTrace(). This produces information about the sequence of methods that were called to get to the point where the exception happened. By default, the information goes to the standard error stream, but overloaded versions allow you to send the results to any other stream as well.

The process of creating your own exceptions can be taken further. You can add extra constructors and members:

 
Sélectionnez

//: c09:ExtraFeatures.java
// Further embellishment of exception classes.
import com.bruceeckel.simpletest.*;
 
class MyException2 extends Exception {
	private int x;
	public MyException2() {}
	public MyException2(String msg) { super(msg); }
	public MyException2(String msg, int x) {
		super(msg);
		this.x = x;
	}
	public int val() { return x; }
	public String getMessage() {
		return "Detail Message: "+ x + " "+ super.getMessage();
	}
}
 
public class ExtraFeatures {
	private static Test monitor = new Test();
	public static void f() throws MyException2 {
		System.out.println("Throwing MyException2 from f()");
		throw new MyException2();
	}
	public static void g() throws MyException2 {
		System.out.println("Throwing MyException2 from g()");
		throw new MyException2("Originated in g()");
	}
	public static void h() throws MyException2 {
		System.out.println("Throwing MyException2 from h()");
		throw new MyException2("Originated in h()", 47);
	}
	public static void main(String[] args) {
		try {
			f();
		} catch(MyException2 e) {
			e.printStackTrace();
		}
		try {
			g();
		} catch(MyException2 e) {
			e.printStackTrace();
		}
		try {
			h();
		} catch(MyException2 e) {
			e.printStackTrace();
			System.err.println("e.val() = " + e.val());
		}
		monitor.expect(new String[] {
			"Throwing MyException2 from f()",
			"MyException2: Detail Message: 0 null",
			"%% \tat ExtraFeatures.f\\(.*\\)",
			"%% \tat ExtraFeatures.main\\(.*\\)",
			"Throwing MyException2 from g()",
			"MyException2: Detail Message: 0 Originated in g()",
			"%% \tat ExtraFeatures.g\\(.*\\)",
			"%% \tat ExtraFeatures.main\\(.*\\)",
			"Throwing MyException2 from h()",
			"MyException2: Detail Message: 47 Originated in h()",
			"%% \tat ExtraFeatures.h\\(.*\\)",
			"%% \tat ExtraFeatures.main\\(.*\\)",
			"e.val() = 47"
		});
	}
} ///:~


A field i has been added, along with a method that reads that value and an additional constructor that sets it. In addition, Throwable.getMessage() has been overridden to produce a more interesting detail message. getMessage() is something like toString() for exception classes.

Since an exception is just another kind of object, you can continue this process of embellishing the power of your exception classes. Keep in mind, however, that all this dressing-up might be lost on the client programmers using your packages, since they might simply look for the exception to be thrown and nothing more. (That's the way most of the Java library exceptions are used.)

9.4. The exception specification

In Java, you're encouraged to inform the client programmer, who calls your method, of the exceptions that might be thrown from your method. This is civilized, because the caller can know exactly what code to write to catch all potential exceptions. Of course, if source code is available, the client programmer could hunt through and look for throw statements, but often a library doesn't come with sources. To prevent this from being a problem, Java provides syntax (and forces you to use that syntax) to allow you to politely tell the client programmer what exceptions this method throws, so the client programmer can handle them. This is the exception specification and it's part of the method declaration, appearing after the argument list.

The exception specification uses an additional keyword, throws, followed by a list of all the potential exception types. So your method definition might look like this:

 
Sélectionnez

void f() throws TooBig, TooSmall, DivZero { //... 


If you say

 
Sélectionnez

void f() { // ...


it means that no exceptions are thrown from the method (except for the exceptions inherited from RuntimeException, which can be thrown anywhere without exception specifications-this will be described later).

You can't lie about an exception specification. If the code within your method causes exceptions, but your method doesn't handle them, the compiler will detect this and tell you that you must either handle the exception or indicate with an exception specification that it may be thrown from your method. By enforcing exception specifications from top to bottom, Java guarantees that a certain level of exception correctness can be ensured at compile time.

There is one place you can lie: You can claim to throw an exception that you really don't. The compiler takes your word for it, and forces the users of your method to treat it as if it really does throw that exception. This has the beneficial effect of being a placeholder for that exception, so you can actually start throwing the exception later without requiring changes to existing code. It's also important for creating abstract base classes and interfaces whose derived classes or implementations may need to throw exceptions.

Exceptions that are checked and enforced at compile time are called checked exceptions.

9.5. Catching any exception

It is possible to create a handler that catches any type of exception. You do this by catching the base-class exception type Exception (there are other types of base exceptions, but Exception is the base that's pertinent to virtually all programming activities):

 
Sélectionnez

catch(Exception e) {
	System.err.println("Caught an exception");
}


This will catch any exception, so if you use it you'll want to put it at the end of your list of handlers to avoid preempting any exception handlers that might otherwise follow it.

Since the Exception class is the base of all the exception classes that are important to the programmer, you don't get much specific information about the exception, but you can call the methods that come from its base type Throwable:

String getMessage()
String getLocalizedMessage()
Gets the detail message, or a message adjusted for this particular locale.

String toString()
Returns a short description of the Throwable, including the detail message if there is one.

void printStackTrace()
void printStackTrace(PrintStream)
void printStackTrace(java.io.PrintWriter)
Prints the Throwable and the Throwable's call stack trace. The call stack shows the sequence of method calls that brought you to the point at which the exception was thrown. The first version prints to standard error, the second and third prints to a stream of your choice (in Chapter 12, you'll understand why there are two types of streams).

Throwable fillInStackTrace()
Records information within this Throwable object about the current state of the stack frames. Useful when an application is rethrowing an error or exception (more about this shortly).

In addition, you get some other methods from Throwable's base type Object (everybody's base type). The one that might come in handy for exceptions is getClass(), which returns an object representing the class of this object. You can in turn query this Class object for its name with getName(). You can also do more sophisticated things with Class objects that aren't necessary in exception handling.

Here's an example that shows the use of the basic Exception methods:

 
Sélectionnez

//: c09:ExceptionMethods.java
// Demonstrating the Exception Methods.
import com.bruceeckel.simpletest.*;
 
public class ExceptionMethods {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		try {
			throw new Exception("My Exception");
		} catch(Exception e) {
			System.err.println("Caught Exception");
			System.err.println("getMessage():" + e.getMessage());
			System.err.println("getLocalizedMessage():" +
				e.getLocalizedMessage());
			System.err.println("toString():" + e);
			System.err.println("printStackTrace():");
			e.printStackTrace();
		}
		monitor.expect(new String[] {
			"Caught Exception",
			"getMessage():My Exception",
			"getLocalizedMessage():My Exception",
			"toString():java.lang.Exception: My Exception",
			"printStackTrace():",
			"java.lang.Exception: My Exception",
			"%% \tat ExceptionMethods.main\\(.*\\)"
		});
	}
} ///:~


You can see that the methods provide successively more information-each is effectively a superset of the previous one.

9.5.1. Rethrowing an exception

Sometimes you'll want to rethrow the exception that you just caught, particularly when you use Exception to catch any exception. Since you already have the reference to the current exception, you can simply rethrow that reference:

 
Sélectionnez

catch(Exception e) {
	System.err.println("An exception was thrown");
	throw e;
}


Rethrowing an exception causes it to go to the exception handlers in the next-higher context. Any further catch clauses for the same try block are still ignored. In addition, everything about the exception object is preserved, so the handler at the higher context that catches the specific exception type can extract all the information from that object.

If you simply rethrow the current exception, the information that you print about that exception in printStackTrace() will pertain to the exception's origin, not the place where you rethrow it. If you want to install new stack trace information, you can do so by calling fillInStackTrace(), which returns a Throwable object that it creates by stuffing the current stack information into the old exception object. Here's what it looks like:

 
Sélectionnez

//: c09:Rethrowing.java
// Demonstrating fillInStackTrace()
import com.bruceeckel.simpletest.*;
 
public class Rethrowing {
	private static Test monitor = new Test();
	public static void f() throws Exception {
		System.out.println("originating the exception in f()");
		throw new Exception("thrown from f()");
	}
	public static void g() throws Throwable {
		try {
			f();
		} catch(Exception e) {
			System.err.println("Inside g(),e.printStackTrace()");
			e.printStackTrace();
			throw e; // 17
			// throw e.fillInStackTrace(); // 18
		}
	}
	public static void
	main(String[] args) throws Throwable {
		try {
			g();
		} catch(Exception e) {
			System.err.println(
				"Caught in main, e.printStackTrace()");
			e.printStackTrace();
		}
		monitor.expect(new String[] {
			"originating the exception in f()",
			"Inside g(),e.printStackTrace()",
			"java.lang.Exception: thrown from f()",
			"%% \tat Rethrowing.f(.*?)",
			"%% \tat Rethrowing.g(.*?)",
			"%% \tat Rethrowing.main(.*?)",
			"Caught in main, e.printStackTrace()",
			"java.lang.Exception: thrown from f()",
			"%% \tat Rethrowing.f(.*?)",
			"%% \tat Rethrowing.g(.*?)",
			"%% \tat Rethrowing.main(.*?)"
		});
	}
} ///:~


The important line numbers are marked as comments. With line 17 uncommented (as shown), the output is as shown, so the exception stack trace always remembers its true point of origin no matter how many times it gets rethrown.

With line 17 commented and line 18 uncommented, fillInStackTrace() is used instead, and the result is:

 
Sélectionnez

originating the exception in f()
Inside g(),e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.f(Rethrowing.java:9)
        at Rethrowing.g(Rethrowing.java:12)
        at Rethrowing.main(Rethrowing.java:23)
Caught in main, e.printStackTrace()
java.lang.Exception: thrown from f()
        at Rethrowing.g(Rethrowing.java:18)
        at Rethrowing.main(Rethrowing.java:23)


(Plus additional complaints from the Test.expect() method.) Because of fillInStackTrace(), line 18 becomes the new point of origin of the exception.

he class Throwable must appear in the exception specification for g() and main() because fillInStackTrace() produces a reference to a Throwable object. Since Throwable is a base class of Exception, it's possible to get an object that's a Throwable but not an Exception, so the handler for Exception in main() might miss it. To make sure everything is in order, the compiler forces an exception specification for Throwable. For example, the exception in the following program is not caught in main():

 
Sélectionnez

//: c09:ThrowOut.java
// {ThrowsException}
public class ThrowOut {
	public static void
	main(String[] args) throws Throwable {
		try {
			throw new Throwable();
		} catch(Exception e) {
			System.err.println("Caught in main()");
		}
	}
} ///:~


It's also possible to rethrow a different exception from the one you caught. If you do this, you get a similar effect as when you use fillInStackTrace()-the information about the original site of the exception is lost, and what you're left with is the information pertaining to the new throw:

 
Sélectionnez

//: c09:RethrowNew.java
// Rethrow a different object from the one that was caught.
// {ThrowsException}
import com.bruceeckel.simpletest.*;
 
class OneException extends Exception {
	public OneException(String s) { super(s); }
}
 
class TwoException extends Exception {
	public TwoException(String s) { super(s); }
}
 
public class RethrowNew {
	private static Test monitor = new Test();
	public static void f() throws OneException {
		System.out.println("originating the exception in f()");
		throw new OneException("thrown from f()");
	}
	public static void
	main(String[] args) throws TwoException {
		try {
			f();
		} catch(OneException e) {
		System.err.println(
			"Caught in main, e.printStackTrace()");
		e.printStackTrace();
			throw new TwoException("from main()");
		}
		monitor.expect(new String[] {
			"originating the exception in f()",
			"Caught in main, e.printStackTrace()",
			"OneException: thrown from f()",
			"\tat RethrowNew.f(RethrowNew.java:18)",
			"\tat RethrowNew.main(RethrowNew.java:22)",
			"Exception in thread \"main\" " +
			"TwoException: from main()",
			"\tat RethrowNew.main(RethrowNew.java:28)"
		});
	}
} ///:~


The final exception knows only that it came from main() and not from f().

You never have to worry about cleaning up the previous exception, or any exceptions for that matter. They're all heap-based objects created with new, so the garbage collector automatically cleans them all up.

9.5.2. Exception chaining

Often you want to catch one exception and throw another, but still keep the information about the originating exception-this is called exception chaining. Prior to JDK 1.4, programmers had to write their own code to preserve the original exception information, but now all Throwable subclasses may take a cause object in their constructor. The cause is intended to be the originating exception, and by passing it in you maintain the stack trace back to its origin, even though you're creating and throwing a new exception at this point.

It's interesting to note that the only Throwable subclasses that provide the cause argument in the constructor are the three fundamental exception classes Error (used by the JVM to report system errors), Exception, and RuntimeException. If you want to chain any other exception types, you do it through the initCause() method rather than the constructor.

Here's an example that allows you to dynamically add fields to a DynamicFields object at run time:

 
Sélectionnez

//: c09:DynamicFields.java
// A Class that dynamically adds fields to itself.
// Demonstrates exception chaining.
// {ThrowsException}
import com.bruceeckel.simpletest.*;
 
class DynamicFieldsException extends Exception {}
 
public class DynamicFields {
	private static Test monitor = new Test();
	private Object[][] fields;
	public DynamicFields(int initialSize) {
		fields = new Object[initialSize][2];
		for(int i = 0; i < initialSize; i++)
			fields[i] = new Object[] { null, null };
	}
	public String toString() {
		StringBuffer result = new StringBuffer();
		for(int i = 0; i < fields.length; i++) {
			result.append(fields[i][0]);
			result.append(": ");
			result.append(fields[i][1]);
			result.append("\n");
		}
		return result.toString();
	}
	private int hasField(String id) {
		for(int i = 0; i < fields.length; i++)
			if(id.equals(fields[i][0]))
				return i;
		return -1;
	}
	private int
	getFieldNumber(String id) throws NoSuchFieldException {
		int fieldNum = hasField(id);
		if(fieldNum == -1)
			throw new NoSuchFieldException();
		return fieldNum;
	}
	private int makeField(String id) {
		for(int i = 0; i < fields.length; i++)
			if(fields[i][0] == null) {
				fields[i][0] = id;
				return i;
			}
		// No empty fields. Add one:
		Object[][]tmp = new Object[fields.length + 1][2];
		for(int i = 0; i < fields.length; i++)
			tmp[i] = fields[i];
		for(int i = fields.length; i < tmp.length; i++)
			tmp[i] = new Object[] { null, null };
		fields = tmp;
		// Reursive call with expanded fields:
		return makeField(id);
	}
	public Object
	getField(String id) throws NoSuchFieldException {
		return fields[getFieldNumber(id)][1];
	}
	public Object setField(String id, Object value)
	throws DynamicFieldsException {
		if(value == null) {
			// Most exceptions don't have a "cause" constructor.
			// In these cases you must use initCause(),
			// available in all Throwable subclasses.
			DynamicFieldsException dfe =
				new DynamicFieldsException();
			dfe.initCause(new NullPointerException());
			throw dfe;
		}
		int fieldNumber = hasField(id);
		if(fieldNumber == -1)
			fieldNumber = makeField(id);
		Object result = null;
		try {
			result = getField(id); // Get old value
		} catch(NoSuchFieldException e) {
			// Use constructor that takes "cause":
			throw new RuntimeException(e);
		}
		fields[fieldNumber][1] = value;
		return result;
	}
	public static void main(String[] args) {
		DynamicFields df = new DynamicFields(3);
		System.out.println(df);
		try {
			df.setField("d", "A value for d");
			df.setField("number", new Integer(47));
			df.setField("number2", new Integer(48));
			System.out.println(df);
			df.setField("d", "A new value for d");
			df.setField("number3", new Integer(11));
			System.out.println(df);
			System.out.println(df.getField("d"));
			Object field = df.getField("a3"); // Exception
		} catch(NoSuchFieldException e) {
			throw new RuntimeException(e);
		} catch(DynamicFieldsException e) {
			throw new RuntimeException(e);
		}
		monitor.expect(new String[] {
			"null: null",
			"null: null",
			"null: null",
			"",
			"d: A value for d",
			"number: 47",
			"number2: 48",
			"",
			"d: A new value for d",
			"number: 47",
			"number2: 48",
			"number3: 11",
			"",
			"A value for d",
			"Exception in thread \"main\" " +
			"java.lang.RuntimeException: " +
			"java.lang.NoSuchFieldException",
			"\tat DynamicFields.main(DynamicFields.java:98)",
			"Caused by: java.lang.NoSuchFieldException",
			"\tat DynamicFields.getFieldNumber(" +
			"DynamicFields.java:37)",
			"\tat DynamicFields.getField(DynamicFields.java:58)",
			"\tat DynamicFields.main(DynamicFields.java:96)"
		});
	}
} ///:~


Each DynamicFields object contains an array of Object-Object pairs. The first object is the field identifier (a String), and the second is the field value, which can be any type except an unwrapped primitive. When you create the object, you make an educated guess about how many fields you need. When you call setField(), it either finds the existing field by that name or creates a new one, and puts in your value. If it runs out of space, it adds new space by creating an array of length one longer and copying the old elements in. If you try to put in a null value, then it throws a DynamicFieldsException by creating one and using initCause() to insert a NullPointerException as the cause.

As a return value, setField() also fetches out the old value at that field location using getField(), which could throw a NoSuchFieldException. If the client programmer calls getField(), then they are responsible for handling NoSuchFieldException, but if this exception is thrown inside setField(), it's a programming error, so the NoSuchFieldException is converted to a RuntimeException using the constructor that takes a cause argument.

9.6. Standard Java exceptions

The Java class Throwable describes anything that can be thrown as an exception. There are two general types of Throwable objects ("types of" = "inherited from"). Error represents compile-time and system errors that you don't worry about catching (except in special cases). Exception is the basic type that can be thrown from any of the standard Java library class methods and from your methods and run-time accidents. So the Java programmer's base type of interest is usually Exception.

The best way to get an overview of the exceptions is to browse the HTML Java documentation that you can download from java.sun.com. It's worth doing this once just to get a feel for the various exceptions, but you'll soon see that there isn't anything special between one exception and the next except for the name. Also, the number of exceptions in Java keeps expanding; basically, it's pointless to print them in a book. Any new library you get from a third-party vendor will probably have its own exceptions as well. The important thing to understand is the concept and what you should do with the exceptions.

The basic idea is that the name of the exception represents the problem that occurred, and the exception name is intended to be relatively self-explanatory. The exceptions are not all defined in java.lang; some are created to support other libraries such as util, net, and io, which you can see from their full class names or what they are inherited from. For example, all I/O exceptions are inherited from java.io.IOException.

9.6.1. The special case of RuntimeException

The first example in this chapter was

 
Sélectionnez

if(t == null)
	throw new NullPointerException();


It can be a bit horrifying to think that you must check for null on every reference that is passed into a method (since you can't know if the caller has passed you a valid reference). Fortunately, you don't-this is part of the standard run-time checking that Java performs for you, and if any call is made to a null reference, Java will automatically throw a NullPointerException. So the above bit of code is always superfluous.

There's a whole group of exception types that are in this category. They're always thrown automatically by Java and you don't need to include them in your exception specifications. Conveniently enough, they're all grouped together by putting them under a single base class called RuntimeException, which is a perfect example of inheritance; It establishes a family of types that have some characteristics and behaviors in common. Also, you never need to write an exception specification saying that a method might throw a RuntimeException (or any type inherited from RuntimeException), because they are unchecked exceptions. Because they indicate bugs, you don't usually catch a RuntimeException-it's dealt with automatically. If you were forced to check for RuntimeExceptions, your code could get too messy. Even though you don't typically catch RuntimeExceptions,in your own packages you might choose to throw some of the RuntimeExceptions.

What happens when you don't catch such exceptions? Since the compiler doesn't enforce exception specifications for these, it's quite plausible that a RuntimeException could percolate all the way out to your main() method without being caught. To see what happens in this case, try the following example:

 
Sélectionnez

//: c09:NeverCaught.java
// Ignoring RuntimeExceptions.
// {ThrowsException}
import com.bruceeckel.simpletest.*;
 
public class NeverCaught {
	private static Test monitor = new Test();
	static void f() {
		throw new RuntimeException("From f()");
	}
	static void g() {
		f();
	}
	public static void main(String[] args) {
		g();
		monitor.expect(new String[] {
			"Exception in thread \"main\" " +
			"java.lang.RuntimeException: From f()",
			"        at NeverCaught.f(NeverCaught.java:7)",
			"        at NeverCaught.g(NeverCaught.java:10)",
			"        at NeverCaught.main(NeverCaught.java:13)"
		});
	}
} ///:~


You can already see that a RuntimeException (or anything inherited from it) is a special case, since the compiler doesn't require an exception specification for these types.

So the answer is: If a RuntimeException gets all the way out to main() without being caught, printStackTrace() is called for that exception as the program exits.

Keep in mind that you can only ignore exceptions of type RuntimeException (and subclasses) in your coding, since all other handling is carefully enforced by the compiler. The reasoning is that a RuntimeException represents a programming error:

  • An error you cannot anticipate. For example, a null reference that is outside of your control.
  • An error that you, as a programmer, should have checked for in your code (such as ArrayIndexOutOfBoundsException where you should have paid attention to the size of the array). An exception that happens from point #1 often becomes an issue for point #2.

You can see what a tremendous benefit it is to have exceptions in this case, since they help in the debugging process.

It's interesting to notice that you cannot classify Java exception handling as a single-purpose tool. Yes, it is designed to handle those pesky run-time errors that will occur because of forces outside your code's control, but it's also essential for certain types of programming bugs that the compiler cannot detect.

9.7. Performing cleanup with finally

There's often some piece of code that you want to execute whether or not an exception is thrown within a try block. This usually pertains to some operation other than memory recovery (since that's taken care of by the garbage collector). To achieve this effect, you use a finally clause (41) at the end of all the exception handlers. The full picture of an exception handling section is thus:

 
Sélectionnez

try {
	// The guarded region: Dangerous activities
	// that might throw A, B, or C 
} catch(A a1) {
	// Handler for situation A
} catch(B b1) {
	// Handler for situation B
} catch(C c1) {
	// Handler for situation C
} finally {
	// Activities that happen every time
}


To demonstrate that the finally clause always runs, try this program:

 
Sélectionnez

//: c09:FinallyWorks.java
// The finally clause is always executed.
import com.bruceeckel.simpletest.*;
 
class ThreeException extends Exception {}
 
public class FinallyWorks {
	private static Test monitor = new Test();
	static int count = 0;
	public static void main(String[] args) {
		while(true) {
			try {
				// Post-increment is zero first time:
				if(count++ == 0)
					throw new ThreeException();
				System.out.println("No exception");
			} catch(ThreeException e) {
				System.err.println("ThreeException");
			} finally {
				System.err.println("In finally clause");
				if(count == 2) break; // out of "while"
			}
	}
		monitor.expect(new String[] {
			"ThreeException",
			"In finally clause",
			"No exception",
			"In finally clause"
		});
	}
} ///:~


From the output, you can see that whether or not an exception is thrown, the finally clause is always executed.

This program also gives a hint for how you can deal with the fact that exceptions in Java (like exceptions in C++) do not allow you to resume back to where the exception was thrown, as discussed earlier. If you place your try block in a loop, you can establish a condition that must be met before you continue the program. You can also add a static counter or some other device to allow the loop to try several different approaches before giving up. This way you can build a greater level of robustness into your programs.

9.7.1. What's finally for?

In a language without garbage collection and without automatic destructor calls (42) , finally is important because it allows the programmer to guarantee the release of memory regardless of what happens in the try block. But Java has garbage collection, so releasing memory is virtually never a problem. Also, it has no destructors to call. So when do you need to use finally in Java?

The finally clause is necessary when you need to set something other than memory back to its original state. This is some kind of cleanup like an open file or network connection, something you've drawn on the screen, or even a switch in the outside world, as modeled in the following example:

 
Sélectionnez

//: c09:Switch.java
public class Switch {
	private boolean state = false;
	public boolean read() { return state; }
	public void on() { state = true; }
	public void off() { state = false; }
} ///:~


 
Sélectionnez

//: c09:OnOffException1.java
public class OnOffException1 extends Exception {} ///:~


 
Sélectionnez

//: c09:OnOffException2.java
public class OnOffException2 extends Exception {} ///:~


 
Sélectionnez

//: c09:OnOffSwitch.java
// Why use finally?
 
public class OnOffSwitch {
	private static Switch sw = new Switch();
	public static void f()
	throws OnOffException1,OnOffException2 {}
	public static void main(String[] args) {
		try {
			sw.on();
			// Code that can throw exceptions...
			f();
			sw.off();
		} catch(OnOffException1 e) {
			System.err.println("OnOffException1");
			sw.off();
		} catch(OnOffException2 e) {
			System.err.println("OnOffException2");
			sw.off();
		}
	}
} ///:~


The goal here is to make sure that the switch is off when main() is completed, so sw.off() is placed at the end of the try block and at the end of each exception handler. But it's possible that an exception could be thrown that isn't caught here, so sw.off() would be missed. However, with finally you can place the cleanup code from a try block in just one place:

 
Sélectionnez

//: c09:WithFinally.java
// Finally Guarantees cleanup.
 
public class WithFinally {
	static Switch sw = new Switch();
	public static void main(String[] args) {
		try {
			sw.on();
			// Code that can throw exceptions...
			OnOffSwitch.f();
		} catch(OnOffException1 e) {
			System.err.println("OnOffException1");
		} catch(OnOffException2 e) {
			System.err.println("OnOffException2");
		} finally {
			sw.off();
		}
	}
} ///:~


Here the sw.off() has been moved to just one place, where it's guaranteed to run no matter what happens.

Even in cases in which the exception is not caught in the current set of catch clauses, finally will be executed before the exception handling mechanism continues its search for a handler at the next higher level:

 
Sélectionnez

//: c09:AlwaysFinally.java
// Finally is always executed.
import com.bruceeckel.simpletest.*;
 
class FourException extends Exception {}
 
public class AlwaysFinally {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		System.out.println("Entering first try block");
		try {
			System.out.println("Entering second try block");
			try {
				throw new FourException();
			} finally {
				System.out.println("finally in 2nd try block");
			}
		} catch(FourException e) {
			System.err.println(
				"Caught FourException in 1st try block");
		} finally {
			System.err.println("finally in 1st try block");
		}
		monitor.expect(new String[] {
			"Entering first try block",
			"Entering second try block",
			"finally in 2nd try block",
			"Caught FourException in 1st try block",
			"finally in 1st try block"
		});
	}
} ///:~


The finally statement will also be executed in situations in which break and continue statements are involved. Note that, along with the labeled break and labeled continue, finally eliminates the need for a goto statement in Java.

9.7.2. Pitfall: the lost exception

Unfortunately, there's a flaw in Java's exception implementation. Although exceptions are an indication of a crisis in your program and should never be ignored, it's possible for an exception to simply be lost. This happens with a particular configuration using a finally clause:

 
Sélectionnez

//: c09:LostMessage.java
// How an exception can be lost.
// {ThrowsException}
import com.bruceeckel.simpletest.*;
 
class VeryImportantException extends Exception {
	public String toString() {
		return "A very important exception!";
	}
}
 
class HoHumException extends Exception {
	public String toString() {
		return "A trivial exception";
	}
}
 
public class LostMessage {
	private static Test monitor = new Test();
	void f() throws VeryImportantException {
		throw new VeryImportantException();
	}
	void dispose() throws HoHumException {
		throw new HoHumException();
	}
	public static void main(String[] args) throws Exception {
		LostMessage lm = new LostMessage();
		try {
			lm.f();
		} finally {
			lm.dispose();
		}
		monitor.expect(new String[] {
			"Exception in thread \"main\" A trivial exception",
			"\tat LostMessage.dispose(LostMessage.java:24)",
			"\tat LostMessage.main(LostMessage.java:31)"
		});  
	}
} ///:~


You can see that there's no evidence of the VeryImportantException, which is simply replaced by the HoHumException in the finally clause. This is a rather serious pitfall, since it means that an exception can be completely lost, and in a far more subtle and difficult-to-detect fashion than the preceding example. In contrast, C++ treats the situation in which a second exception is thrown before the first one is handled as a dire programming error. Perhaps a future version of Java will repair this problem (on the other hand, you will typically wrap any method that throws an exception, such as dispose(), inside a try-catch clause).

9.8. Exception restrictions

When you override a method, you can throw only the exceptions that have been specified in the base-class version of the method. This is a useful restriction, since it means that code that works with the base class will automatically work with any object derived from the base class (a fundamental OOP concept, of course), including exceptions.

This example demonstrates the kinds of restrictions imposed (at compile time) for exceptions:

 
Sélectionnez

//: c09:StormyInning.java
// Overridden methods may throw only the exceptions
// specified in their base-class versions, or exceptions
// derived from the base-class exceptions.
 
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
 
abstract class Inning {
	public Inning() throws BaseballException {}
	public void event() throws BaseballException {
		// Doesn't actually have to throw anything
	}
	public abstract void atBat() throws Strike, Foul;
	public void walk() {} // Throws no checked exceptions
}
 
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
 
interface Storm {
	public void event() throws RainedOut;
	public void rainHard() throws RainedOut;
}
 
public class StormyInning extends Inning implements Storm {
	// OK to add new exceptions for constructors, but you
	// must deal with the base constructor exceptions:
	public StormyInning() 
		throws RainedOut, BaseballException {}
	public StormyInning(String s) 
		throws Foul, BaseballException {}
	// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
	// Interface CANNOT add exceptions to existing
	// methods from the base class:
//! public void event() throws RainedOut {}
	// If the method doesn't already exist in the
	// base class, the exception is OK:
	public void rainHard() throws RainedOut {}
	// You can choose to not throw any exceptions,
	// even if the base version does:
	public void event() {}
	// Overridden methods can throw inherited exceptions:
	public void atBat() throws PopFoul {}
	public static void main(String[] args) {
		try {
			StormyInning si = new StormyInning();
			si.atBat();
		} catch(PopFoul e) {
			System.err.println("Pop foul");
		} catch(RainedOut e) {
			System.err.println("Rained out");
		} catch(BaseballException e) {
			System.err.println("Generic baseball exception");
		}
		// Strike not thrown in derived version.
		try {
			// What happens if you upcast?
			Inning i = new StormyInning();
			i.atBat();
			// You must catch the exceptions from the
			// base-class version of the method:
		} catch(Strike e) {
			System.err.println("Strike");
		} catch(Foul e) {
			System.err.println("Foul");
		} catch(RainedOut e) {
			System.err.println("Rained out");
		} catch(BaseballException e) {
			System.err.println("Generic baseball exception");
		}
	}
} ///:~


In Inning, you can see that both the constructor and the event() method say they will throw an exception, but they never do. This is legal because it allows you to force the user to catch any exceptions that might be added in overridden versions of event(). The same idea holds for abstract methods, as seen in atBat().

The interface Storm is interesting because it contains one method (event()) that is defined in Inning, and one method that isn't. Both methods throw a new type of exception, RainedOut. When StormyInning extends Inning and implements Storm, you'll see that the event() method in Storm cannot change the exception interface of event() in Inning. Again, this makes sense because otherwise you'd never know if you were catching the correct thing when working with the base class. Of course, if a method described in an interface is not in the base class, such as rainHard(),then there's no problem if it throws exceptions.

The restriction on exceptions does not apply to constructors. You can see in StormyInning that a constructor can throw anything it wants, regardless of what the base-class constructor throws. However, since a base-class constructor must always be called one way or another (here, the default constructor is called automatically), the derived-class constructor must declare any base-class constructor exceptions in its exception specification. Note that a derived-class constructor cannot catch exceptions thrown by its base-class constructor.

The reason StormyInning.walk() will not compile is that it throws an exception, but Inning.walk() does not. If this were allowed, then you could write code that called Inning.walk() and that didn't have to handle any exceptions, but then when you substituted an object of a class derived from Inning, exceptions would be thrown so your code would break. By forcing the derived-class methods to conform to the exception specifications of the base-class methods, substitutability of objects is maintained.

The overridden event() method shows that a derived-class version of a method may choose not to throw any exceptions, even if the base-class version does. Again, this is fine since it doesn't break any code that is written-assuming the base-class version throws exceptions. Similar logic applies to atBat(), which throws PopFoul, an exception that is derived from Foul thrown by the base-class version of atBat(). This way, if you write code that works with Inning and calls atBat(), you must catch the Foul exception. Since PopFoul is derived from Foul, the exception handler will also catch PopFoul.

The last point of interest is in main(). Here, you can see that if you're dealing with exactly a StormyInning object, the compiler forces you to catch only the exceptions that are specific to that class, but if you upcast to the base type, then the compiler (correctly) forces you to catch the exceptions for the base type. All these constraints produce much more robust exception-handling code. (43)

It's useful to realize that although exception specifications are enforced by the compiler during inheritance, the exception specifications are not part of the type of a method, which comprises only the method name and argument types. Therefore, you cannot overload methods based on exception specifications. In addition, just because an exception specification exists in a base-class version of a method doesn't mean that it must exist in the derived-class version of the method. This is quite different from inheritance rules, where a method in the base class must also exist in the derived class. Put another way, the "exception specification interface" for a particular method may narrow during inheritance and overriding, but it may not widen-this is precisely the opposite of the rule for the class interface during inheritance.

9.9. Constructors

When writing code with exceptions, it's particularly important that you always ask "If an exception occurs, will this be properly cleaned up?" Most of the time you're fairly safe, but in constructors there's a problem. The constructor puts the object into a safe starting state, but it might perform some operation-such as opening a file-that doesn't get cleaned up until the user is finished with the object and calls a special cleanup method. If you throw an exception from inside a constructor, these cleanup behaviors might not occur properly. This means that you must be especially diligent while you write your constructor.

Since you've just learned about finally, you might think that it is the correct solution. But it's not quite that simple, because finally performs the cleanup code every time, even in the situations in which you don't want the cleanup code executed until the cleanup method runs. Thus, if you do perform cleanup in finally, you must set some kind of flag when the constructor finishes normally so that you don't do anything in the finally block if the flag is set. Because this isn't particularly elegant (you are coupling your code from one place to another), it's best if you try to avoid performing this kind of cleanup in finally unless you are forced to.

In the following example, a class called InputFile is created that opens a file and allows you to read it one line (converted into a String) at a time. It uses the classes FileReader and BufferedReader from the Java standard I/O library that will be discussed in Chapter 12, but which are simple enough that you probably won't have any trouble understanding their basic use:

 
Sélectionnez

//: c09:Cleanup.java
// Paying attention to exceptions in constructors.
import com.bruceeckel.simpletest.*;
import java.io.*;
 
class InputFile {
	private BufferedReader in;
	public InputFile(String fname) throws Exception {
		try {
			in = new BufferedReader(new FileReader(fname));
			// Other code that might throw exceptions
		} catch(FileNotFoundException e) {
			System.err.println("Could not open " + fname);
			// Wasn't open, so don't close it
			throw e;
		} catch(Exception e) {
			// All other exceptions must close it
			try {
				in.close();
			} catch(IOException e2) {
				System.err.println("in.close() unsuccessful");
			}
			throw e; // Rethrow
		} finally {
			// Don't close it here!!!
		}
	}
	public String getLine() {
		String s;
		try {
			s = in.readLine();
		} catch(IOException e) {
			throw new RuntimeException("readLine() failed");
		}
		return s;
	}
	public void dispose() {
		try {
			in.close();
			System.out.println("dispose() successful");
		} catch(IOException e2) {
			throw new RuntimeException("in.close() failed");
		}
	}
}
 
public class Cleanup {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		try {
			InputFile in = new InputFile("Cleanup.java");
			String s;
			int i = 1;
			while((s = in.getLine()) != null)
				; // Perform line-by-line processing here...
			in.dispose();
		} catch(Exception e) {
			System.err.println("Caught Exception in main");
			e.printStackTrace();
		}
		monitor.expect(new String[] {
			"dispose() successful"
		});
	}
} ///:~


The constructor for InputFile takes a String argument, which is the name of the file you want to open. Inside a try block, it creates a FileReader using the file name. A FileReader isn't particularly useful until you turn around and use it to create a BufferedReader that you can actually talk to-notice that one of the benefits of InputFile is that it combines these two actions.

If the FileReader constructor is unsuccessful, it throws a FileNotFoundException, which must be caught separately. This is the one case in which you don't want to close the file, because it wasn't successfully opened. Any other catch clauses must close the file because it was opened by the time those catch clauses are entered. (Of course, this is trickier if more than one method can throw a FileNotFoundException. In that case, you might want to break things into several try blocks.) The close() method might throw an exception so it is tried and caught even though it's within the block of another catch clause-it's just another pair of curly braces to the Java compiler. After performing local operations, the exception is rethrown, which is appropriate because this constructor failed, and you wouldn't want the calling method to assume that the object had been properly created and was valid.

In this example, which doesn't use the aforementioned flagging technique, the finally clause is definitely not the place to close() the file, since that would close it every time the constructor completed. Because we want the file to be open for the useful lifetime of the InputFile object, this would not be appropriate.

The getLine() method returns a String containing the next line in the file. It calls readLine(), which can throw an exception, but that exception is caught so getLine() doesn't throw any exceptions. One of the design issues with exceptions is whether to handle an exception completely at this level, to handle it partially and pass the same exception (or a different one) on, or whether to simply pass it on. Passing it on, when appropriate, can certainly simplify coding. In this situation, the getLine() method converts the exception to a RuntimeException to indicate a programming error.

The dispose() method must be called by the user when finished using the InputFile object. This will release the system resources (such as file handles) that are used by the BufferedReader and/or FileReader objects. You don't want to do this until you're finished with the InputFile object, at the point you're going to let it go. You might think of putting such functionality into a finalize() method, but as mentioned in Chapter 4, you can't always be sure that finalize() will be called (even if you can be sure that it will be called, you don't know when). This is one of the downsides to Java; All cleanup-other than memory cleanup-doesn't happen automatically, so you must inform the client programmer that they are responsible, and possibly guarantee that cleanup occurs using finalize().

In Cleanup.java an InputFile is created to open the same source file that creates the program, the file is read in a line at a time, and line numbers are added. All exceptions are caught generically in main(), although you could choose greater granularity.

One of the benefits of this example is to show you why exceptions are introduced at this point in the book-there are many libraries (like I/O, mentioned earlier) that you can't use without dealing with exceptions. Exceptions are so integral to programming in Java, especially because the compiler enforces them, that you can accomplish only so much without knowing how to work with them.

9.10. Exception matching

When an exception is thrown, the exception handling system looks through the "nearest" handlers in the order they are written. When it finds a match, the exception is considered handled, and no further searching occurs.

Matching an exception doesn't require a perfect match between the exception and its handler. A derived-class object will match a handler for the base class, as shown in this example:

 
Sélectionnez

//: c09:Human.java
// Catching exception hierarchies.
import com.bruceeckel.simpletest.*;
 
class Annoyance extends Exception {}
class Sneeze extends Annoyance {}
 
public class Human {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		try {
			throw new Sneeze();
		} catch(Sneeze s) {
			System.err.println("Caught Sneeze");
		} catch(Annoyance a) {
			System.err.println("Caught Annoyance");
		}
		monitor.expect(new String[] {
			"Caught Sneeze"
		});
	}
} ///:~


The Sneeze exception will be caught by the first catch clause that it matches, which is the first one, of course. However, if you remove the first catch clause, leaving only:

 
Sélectionnez

try {
	throw new Sneeze();
} catch(Annoyance a) {
	System.err.println("Caught Annoyance");
}


the code will still work because it's catching the base class of Sneeze. Put another way, catch(Annoyance e) will catch an Annoyance or any class derived from it. This is useful because if you decide to add more derived exceptions to a method, then the client programmer's code will not need changing as long as the client catches the base class exceptions.

If you try to "mask" the derived-class exceptions by putting the base-class catch clause first, like this:

 
Sélectionnez

try {
	throw new Sneeze();
} catch(Annoyance a) {
	System.err.println("Caught Annoyance");
} catch(Sneeze s) {
	System.err.println("Caught Sneeze");
}


the compiler will give you an error message, since it sees that the Sneeze catch-clause can never be reached.

9.11. Alternative approaches

An exception-handling system is a trap door that allows your program to abandon execution of the normal sequence of statements. The trap door is used when an "exceptional condition" occurs, such that normal execution is no longer possible or desirable. Exceptions represent conditions that the current method is unable to handle. The reason exception handling systems were developed is because the approach of dealing with each possible error condition produced by each function call was too onerous, and programmers simply weren't doing it. As a result, they were ignoring the errors. It's worth observing that the issue of programmer convenience in handling errors was a prime motivation for exceptions in the first place.

One of the important guidelines in exception handling is "don't catch an exception unless you know what to do with it." In fact, one of the important goals of exception handling is to move the error-handling code away from the point where the errors occur. This allows you to focus on what you want to accomplish in one section of your code, and how you're going to deal with problems in a distinct separate section of your code. As a result, your mainline code is not cluttered with error-handling logic, and it's much easier to understand and maintain.

Checked exceptions complicate this scenario a bit, because they force you to add catch clauses in places where you may not be ready to handle an error. This results in the "harmful if swallowed" problem:

 
Sélectionnez

try {
	// ... to do something useful
} catch(ObligatoryException e) {} // Gulp!


Programmers (myself included, in the first edition of this book) would just do the simplest thing, and swallow the exception-often unintentionally, but once you do it, the compiler has been satisfied, so unless you remember to revisit and correct the code, the exception will be lost. The exception happens, but it vanishes completely when swallowed. Because the compiler forces you to write code right away to handle the exception, this seems like the easiest solution even though it's probably the worst thing you can do.

Horrified upon realizing that I had done this, in the second edition I "fixed" the problem by printing the stack trace inside the handler (as is still seen-appropriately-in a number of examples in this chapter). While this is useful to trace the behavior of exceptions, it still indicates that you don't really know what to do with the exception at that point in your code. In this section we'll look at some of the issues and complications arising from checked exceptions, and options that you have when dealing with them.

This topic seems simple. But it is not only complicated, it is also an issue of some volatility. There are people who are staunchly rooted on either side of the fence and who feel that the correct answer (theirs) is blatantly obvious. I believe the reason for one of these positions is the distinct benefit seen in going from a poorly-typed language like pre-ANSI C to a strong, statically-typed language (that is, checked at compile-time) like C++ or Java. When you make that transition (as I did), the benefits are so dramatic that it can seem like strong static type checking is always the best answer to most problems. My hope is to relate a little bit of my own evolution, that has brought the absolute value of strong static type checking into question; clearly, it's very helpful much of the time, but there's a fuzzy line we cross when it begins to get in the way and become a hindrance (one of my favorite quotes is: "All models are wrong. Some are useful.").

9.11.1. History

Exception handling originated in systems like PL/1 and Mesa, and later appeared in CLU, Smalltalk, Modula-3, Ada, Eiffel, C++, Python, Java, and the post-Java languages Ruby and C#. The Java design is similar to C++, except in places where the Java designers felt that the C++ design caused problems.

To provide programmers with a framework that they were more likely to use for error handling and recovery, exception handling was added to C++ rather late in the standardization process, promoted by Bjarne Stroustrup, the language's original author. The model for C++ exceptions came primarily from CLU. However, other languages existed at that time that also supported exception handling: Ada, Smalltalk (both of which had exceptions but no exception specifications) and Modula-3 (which included both exceptions and specifications).

In their seminal paper (44) on the subject, Liskov and Snyder note that a major defect of languages like C that report errors in a transient fashion is that:

"...every invocation must be followed by a conditional test to determine what the outcome was. This requirement leads to programs that are difficult to read, and probably inefficient as well, thus discouraging programmers from signaling and handling exceptions."

Note that one of the original motivations of exception handling was to prevent this requirement, but with checked exceptions in Java we commonly see exactly this kind of code. They go on to say:

"...requiring that the text of a handler be attached to the invocation that raises the exception would lead to unreadable programs in which expressions were broken up with handlers."

Following the CLU approach when designing C++ exceptions, Stroustrup stated that the goal was to reduce the amount of code required to recover from errors. I believe that he was observing that programmers were typically not writing error-handling code in C because the amount and placement of such code was daunting and distracting. As a result, they were used to doing it the C way, ignoring errors in code and using debuggers to track down problems. To use exceptions, these C programmers had to be convinced to write "additional" code that they weren't normally writing. Thus, to draw them into a better way of handling errors, the amount of code they would need to "add" must not be onerous. I think it's important to keep this goal in mind when looking at the effects of checked exceptions in Java.

C++ brought an additional idea over from CLU: the exception specification, to programmatically state in the method signature what exceptions may result from calling that method. The exception specification really has two purposes. It can say "I'm originating this exception in my code, you handle it." But it can also mean "I'm ignoring this exception that can occur as a result of my code, you handle it." We've been focusing on the "you handle it" part when looking at the mechanics and syntax of exceptions, but here I'm particularly interested in the fact that often we ignore exceptions and that's what the exception specification can state.

In C++ the exception specification is not part of the type information of a function. The only compile-time checking is to ensure that exception specifications are used consistently; for example, if a function or method throws exceptions, then the overloaded or derived versions must also throw those exceptions. Unlike Java, however, no compile-time checking occurs to determine whether or not the function or method will actually throw that exception, or whether the exception specification is complete (that is, whether it accurately describes all exceptions that may be thrown). That validation does happen, but only at run time. If an exception is thrown that violates the exception specification, the C++ program will call the standard library function unexpected().

It is interesting to note that, because of the use of templates, exception specifications are not used at all in the standard C++ library. Exception specifications, then, may have a significant impact on the design of Java generics (Java's version of C++ templates, expected to appear in JDK 1.5).

9.11.2. Perspectives

First, it's worth noting that Java effectively invented the checked exception (clearly inspired by C++ exception specifications and the fact that C++ programmers typically don't bother with them). It has been an experiment, which no language since has chosen to duplicate.

Secondly, checked exceptions appear to be an obvious good thing when seen in introductory examples and in small programs. It has been suggested that the subtle difficulties begin to appear when programs start to get large. Of course, largeness usually doesn't happen overnight; it creeps. Languages that may not be suited for large-scale projects are used for small projects that grow, and at some point we realize that things have gone from manageable to difficult. This is what I'm suggesting may be the case with too much type checking; in particular, with checked exceptions.

The scale of the program seems to be a significant issue. This is a problem because most discussions tend to use small programs as demonstrations. One of the C# designers observed that:

"Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result-decreased productivity and little or no increase in code quality." (45)

In reference to uncaught exceptions, the CLU creators stated:

"We felt it was unrealistic to require the programmer to provide handlers in situations where no meaningful action can be taken." (46)

When explaining why a function declaration with no specification means that it can throw any exception, rather than no exceptions, Stroustrup states:

"However, that would require exception specifications for essentially every function, would be a significant cause for recompilation, and would inhibit cooperation with software written in other languages. This would encourage programmers to subvert the exception-handling mechanisms and to write spurious code to suppress exceptions. It would provide a false sense of security to people who failed to notice the exception." (47)

We see this very behavior-subverting the exceptions-happening with checked exceptions in Java.

Martin Fowler (author of UML Distilled, Refactoring, and Analysis Patterns) wrote the following to me:

"...on the whole I think that exceptions are good, but Java checked exceptions are more trouble than they are worth."

I now think that Java's important step was to unify the error reporting model, so that all errors are reported using exceptions. This wasn't happening with C++, because for backward compatibility with C the old model of just ignoring errors was still available. But if you have consistent reporting with exceptions, then the exceptions can be used if desired, and if not, they will propagate out to the highest level (the console or other container program). When Java changed the C++ model so that exceptions were the only way to report errors, the extra enforcement of checked exceptions may have become less necessary.

In the past, I have been a strong believer that both checked exceptions and strong static type checking were essential to robust program development. However, both anecdotal and direct experience (48) with languages that are more dynamic than static have lead me to think that the great benefits actually come from:

  • A unified error-reporting model via exceptions, regardless of whether the programmer is forced by the compiler to handle them.
  • Type checking, regardless of when it takes place. That is, as long as proper use of a type is enforced, it doesn't matter if it happens at compile time or run time.

On top of this, there are very significant productivity benefits to reducing the compile-time constraints upon the programmer. Indeed, reflection (and eventually, generics) is required to compensate for the over-constraining nature of strong static typing, as you shall see in the next chapter and in a number of examples throughout the book.

I've already been told by some that what I say here constitutes blasphemy, and by uttering these words my reputation will be destroyed, civilizations will fall, and a higher percentage of programming projects will fail. The belief that the compiler can save your project by pointing out errors at compile time runs strong, but it's even more important to realize the limitation of what the compiler is able to do; in Chapter 15, I emphasize the value of an automated build process and unit testing, which give you far more leverage than you get by trying to turn everything into a syntax error. It's worth keeping in mind that:

A good programming language is one that helps programmers write good programs. No programming language will prevent its users from writing bad programs. (49)

In any event, the likelihood of checked exceptions ever being removed from Java seems dim. It would be too radical of a language change, and proponents within Sun appear to be quite strong. Sun has a history and policy of absolute backwards compatibility-to give you a sense of this, virtually all Sun software runs on all Sun hardware, no matter how old. However, if you find that some checked exceptions are getting in your way, or especially if you find yourself being forced to catch exceptions, but you don't know what to do with them, there are some alternatives.

9.11.3. Passing exceptions to the console

In simple programs, like many of those in this book, the easiest way to preserve the exceptions without writing a lot of code is to pass them out of main() to the console. For example, if you want to open a file for reading (something you'll learn about in detail in Chapter 12), you must open and close a FileInputStream, which throws exceptions. For a simple program, you can do this (you'll see this approach used in numerous places throughout this book):

 
Sélectionnez

//: c09:MainException.java
import java.io.*;
 
public class MainException {
	// Pass all exceptions to the console:
	public static void main(String[] args) throws Exception {
		// Open the file:
		FileInputStream file =
			new FileInputStream("MainException.java");
		// Use the file ...
		// Close the file:
		file.close();
	}
} ///:~


Note that main() is also a method that may have an exception specification, and here the type of exception is Exception,the root class of all checked exceptions. By passing it out to the console, you are relieved from writing try-catch clauses within the body of main(). (Unfortunately, file I/O is significantly more complex than it would appear to be from this example, so don't get too excited until after you've read Chapter 12).

9.11.4. Converting checked to unchecked exceptions

Throwing an exception from main() is convenient when you're writing a main(), but not generally useful. The real problem is when you are writing an ordinary method body, and you call another method and realize "I have no idea what to do with this exception here, but I don't want to swallow it or print some banal message." With JDK 1.4 chained exceptions, a new and simple solution prevents itself. You simply "wrap" a checked exception inside a RuntimeException, like this:

 
Sélectionnez

try {
	// ... to do something useful
} catch(IDontKnowWhatToDoWithThisCheckedException e) {
	throw new RuntimeException(e);
}


This seems to be an ideal solution if you want to "turn off" the checked exception-you don't swallow it, and you don't have to put it in your method's exception specification, but because of exception chaining you don't lose any information from the original exception.

This technique provides the option to ignore the exception and let it bubble up the call stack without being required to write try-catch clauses and/or exception specifications. However, you may still catch and handle the specific exception by using getCause(), as seen here:

 
Sélectionnez

//: c09:TurnOffChecking.java
// "Turning off" Checked exceptions.
import com.bruceeckel.simpletest.*;
import java.io.*;
 
class WrapCheckedException {
	void throwRuntimeException(int type) {
		try {
			switch(type) {
				case 0: throw new FileNotFoundException();
				case 1: throw new IOException();
				case 2: throw new RuntimeException("Where am I?");
				default: return;
			}
		} catch(Exception e) { // Adapt to unchecked:
			throw new RuntimeException(e);
		}
	}
}
 
class SomeOtherException extends Exception {}
 
public class TurnOffChecking {
	private static Test monitor = new Test();
	public static void main(String[] args) {
		WrapCheckedException wce = new WrapCheckedException();
		// You can call f() without a try block, and let
		// RuntimeExceptions go out of the method:
		wce.throwRuntimeException(3);
		// Or you can choose to catch exceptions:
		for(int i = 0; i < 4; i++)
			try {
				if(i < 3)
					wce.throwRuntimeException(i);
				else
					throw new SomeOtherException();
			} catch(SomeOtherException e) {
				System.out.println("SomeOtherException: " + e);
			} catch(RuntimeException re) {
				try {
					throw re.getCause();
				} catch(FileNotFoundException e) {
					System.out.println(
						"FileNotFoundException: " + e);
				} catch(IOException e) {
					System.out.println("IOException: " + e);
				} catch(Throwable e) {
					System.out.println("Throwable: " + e);
				}
			}
			monitor.expect(new String[] {
				"FileNotFoundException: " +
				"java.io.FileNotFoundException",
				"IOException: java.io.IOException",
				"Throwable: java.lang.RuntimeException: Where am I?",
				"SomeOtherException: SomeOtherException"
		});
	}
} ///:~


WrapCheckedException.throwRuntimeException() contains code that generates different types of exceptions. These are caught and wrapped inside RuntimeException objects, so they become the "cause" of those exceptions.

In TurnOffChecking, you can see that it's possible to call throwRuntimeException() with no try block because the method does not throw any checked exceptions. However, when you're ready to catch exceptions, you still have the ability to catch any exception you want by putting your code inside a try block. You start by catching all the exceptions you explicitly know might emerge from the code in your try block-in this case, SomeOtherException is caught first. Lastly, you catch RuntimeException and throw the result of getCause() (the wrapped exception). This extracts the originating exceptions, which can then be handled in their own catch clauses.

The technique of wrapping a checked exception in a RuntimeException will be used when appropriate throughout the rest of this book.

9.12. Exception guidelines

Use exceptions to:

  • Handle problems at the appropriate level. (Avoid catching exceptions unless you know what to do with them).
  • Fix the problem and call the method that caused the exception again.
  • Patch things up and continue without retrying the method.
  • Calculate some alternative result instead of what the method was supposed to produce.
  • Do whatever you can in the current context and rethrow the same exception to a higher context.
  • Do whatever you can in the current context and throw a different exception to a higher context.
  • Terminate the program.
  • Simplify. (If your exception scheme makes things more complicated, then it is painful and annoying to use.)
  • Make your library and program safer. (This is a short-term investment for debugging, and a long-term investment for application robustness.)

9.13. Summary

Improved error recovery is one of the most powerful ways that you can increase the robustness of your code. Error recovery is a fundamental concern for every program you write, but it's especially important in Java, where one of the primary goals is to create program components for others to use. To create a robust system, each component must be robust. By providing a consistent error-reporting model with exceptions, Java allows components to reliably communicate problems to client code.

The goals for exception handling in Java are to simplify the creation of large, reliable programs using less code than currently possible, and to do so with more confidence that your application doesn't have an unhandled error. Exceptions are not terribly difficult to learn, and are one of those features that provide immediate and significant benefits to your project.

9.14. Exercises

Solutions to selected exercises can be found in the electronic document The Thinking in Java Annotated Solution Guide, available for a small fee from www.BruceEckel.com.

  1. Create a class with a main() that throws an object of class Exception inside a try block. Give the constructor for Exception a String argument. Catch the exception inside a catch clause and print the String argument. Add a finally clause and print a message to prove you were there.
  2. Create your own exception class using the extends keyword. Write a constructor for this class that takes a String argument and stores it inside the object with a String reference. Write a method that prints out the stored String. Create a try-catch clause to exercise your new exception.
  3. Write a class with a method that throws an exception of the type created in Exercise 2. Try compiling it without an exception specification to see what the compiler says. Add the appropriate exception specification. Try out your class and its exception inside a try-catch clause.
  4. Define an object reference and initialize it to null. Try to call a method through this reference. Now wrap the code in a try-catch clause to catch the exception.
  5. Create a class with two methods, f() and g(). In g(), throw an exception of a new type that you define. In f(), call g(), catch its exception and, in the catch clause, throw a different exception (of a second type that you define). Test your code in main().
  6. Repeat the previous exercise, but inside the catch clause, wrap g()'s exception in a RuntimeException.
  7. Create three new types of exceptions. Write a class with a method that throws all three. In main(), call the method but only use a single catch clause that will catch all three types of exceptions.
  8. Write code to generate and catch an ArrayIndexOutOfBoundsException.
  9. Create your own resumption-like behavior by using a while loop that repeats until an exception is no longer thrown.
  10. Create a three-level hierarchy of exceptions. Now create a base-class A with a method that throws an exception at the base of your hierarchy. Inherit B from A and override the method so it throws an exception at level two of your hierarchy. Repeat by inheriting class C from B. In main(), create a C and upcast it to A, then call the method.
  11. Demonstrate that a derived-class constructor cannot catch exceptions thrown by its base-class constructor.
  12. Show that OnOffSwitch.java can fail by throwing a RuntimeException inside the try block.
  13. Show that WithFinally.java doesn't fail by throwing a RuntimeException inside the try block.
  14. Modify Exercise 7 by adding a finally clause. Verify that your finally clause is executed, even if a NullPointerException is thrown.
  15. Create an example where you use a flag to control whether cleanup code is called, as described in the second paragraph after the heading "Constructors."
  16. Modify StormyInning.java by adding an UmpireArgument exception type and methods that throw this exception. Test the modified hierarchy.
  17. Remove the first catch clause in Human.java and verify that the code still compiles and runs properly.
  18. Add a second level of exception loss to LostMessage.java so that the HoHumException is itself replaced by a third exception.
  19. Add an appropriate set of exceptions to c08:GreenhouseControls.java.
  20. Add an appropriate set of exceptions to c08:Sequence.java.
  21. Change the file name string in MainException.java to name a file that doesn't exist. Run the program and note the result.

prcdentsommairesuivant
The C programmer can look up the return value of printf() for an example of this.
C++ exception handling does not have the finally clause because it relies on destructors to accomplish this sort of cleanup.
A destructor is a function that's always called when an object becomes unused. You always know exactly where and when the destructor gets called. C++ has automatic destructor calls, and C# (which is much more like Java) has a way that automatic destruction can occur.
ISO C++ added similar constraints that require derived-method exceptions to be the same as, or derived from, the exceptions thrown by the base-class method. This is one case in which C++ is actually able to check exception specifications at compile time.
Barbara Liskov and Alan Snyder: Exception Handling in CLU, IEEE Transactions on Software Engineering, Vol. SE-5, No. 6, November 1979. This paper is not available on the Internet, only in print form so you'll have to contact a library to get a copy.
http://discuss.develop.com/archives/wa.exe?A2=ind0011A&L=DOTNET&P=R32820
ibid
Bjarne Stroustrup, The C++ Programming Language, 3rd edition, Addison-Wesley 1997, pp 376.
Indirectly with Smalltalk via conversations with many experienced programmers in that language; directly with Python (www.Python.org).
(Kees Koster, designer of the CDL language, as quoted by Bertrand Meyer, designer of the Eiffel Language). http://www.elj.com/elj/v1/n1/bm/right/.

Vous avez aim ce tutoriel? Alors partagez-le en cliquant sur les boutons suivants: Viadeo Twitter Facebook Share on Google+   

Ce document est issu de http://www.developpez.com et reste la proprit exclusive de son auteur. La copie, modification et/ou distribution par quelque moyen que ce soit est soumise l'obtention pralable de l'autorisation de l'auteur.