The Java programming course


Lecture 6: Exceptions and Assertions

During execution, applications can run into many kinds of errors of varying degrees of severity. When methods are invoked on an object, the object can discover internal state problems (inconsistent values of variables), detect errors with objects or data it manipulates (such as a file or network address), determine that it is violating its basic contract (such as reading data from an already closed stream), and so on.

Exceptions provide a clean way to check for errors without cluttering code. Exceptions also provide a mechanism to signal errors directly rather than indirectly with flags or side effects such as fields that must be checked. Exceptions make the error conditions that a method can signal an explicit part of the method's contract. The list of exceptions can be seen by the programmer, checked by the compiler, and preserved (if needed) by extended classes that override the method.

Creating Exception Types

Exceptions are objects. All exception typesthat is, any class designed for throwable objectsmust extend the class Throwable or one of its subclasses. The Throwable class contains a string that can be used to describe the exceptionit is set via a constructor parameter and may be retrieved with the getMessage method. By convention, most new exception types extend Exception, a subclass of Thtowable. Exception types are not allowed to be generic types.

Exceptions are primarily checked exceptions, meaning that the compiler checks that your methods throw only the exceptions they have declared themselves to throw. The standard runtime exceptions and errors extend one of the classes RuntimeException and Error, making them unchecked exceptions.

Checked exceptions represent conditions that, although exceptional, can reasonably be expected to occur, and if they do occur must be dealt with in some way. Making these exceptions checked documents the existence of the exception and ensures that the caller of a method deals with the exception in some way - or at least consciously chooses to ignore it.

Unchecked runtime exceptions represent conditions that, generally speaking, reflect errors in your program's logic and cannot be reasonably recovered from at run time. For example, the ArrayIndexOutOfBoundsException thrown when you access outside the bounds of an array tells you that your program calculated an index incorrectly, or failed to verify a value to be used as an index. These are errors that should be corrected in the program code. Given that you can make errors writing any statement it would be totally impractical to have to declare or catch all the exceptions that could arise from those errors - hence they are unchecked.

The Error class, through its subclasses, defines a range of errors that indicate something has failed in the virtual machine itself (VirtualMachineError), or in the virtual machine's attempt to execute your application (LinkageError). These are also unchecked because they are beyond the applications ability to control, or handle. Application code should rarely, if ever, throw these error exceptions directly.

Nearly all exceptions you create should extend Exception, making them checked exceptions. These new checked exceptions represent the exceptional conditions that can arise in your library or application. You can usually handle any runtime problems that arise in your code by throwing one of the existing runtime exception classes - there are sufficient of these that one of them is usually a good match to the kind of error that has occurred, or else the error is sufficiently general that throwing an instance of RuntimeException itself is sufficient. Occasionally, you may want to extend an existing runtime exception class to provide additional information about the error that occurred. Even more rarely, you might define a new class of runtime exception that is specific to your application domain. Just remember that when it comes to unchecked exceptions, proper usage relies on good documentation because the compiler will not be able to assist you.

Sometimes it is useful to have more data to describe the exceptional condition than what is in the Exception class. In such cases, you can extend Exception to create a class that contains the added data (usually set in the constructor).

For example, suppose a replaceValue method is added to the Attributed interface discussed earlier. This method replaces the current value of a named attribute with a new value. If the named attribute doesn't exist, an exception should be thrown, because it is reasonable to assume that one should replace only existing attributes. That exception should contain the name of the attribute. To represent the exception, we create the NoSuchAttributeException class:

public class NoSuchAttributeException extends Exception { public final String attrName; public NoSuchAttributeException (String name) { super("No attribute named \"" + name + "\" found"); attrName = name; } }

Another reason to create a new exception type is that the type of the exception is an important part of the exception data, because exceptions are caught according to their type. For this reason, you would invent NoSuchAttributeException even if you did not want to add data. In this way, a programmer who cared only about such an exception could catch it exclusive of other exceptions that might be generated either by the methods of the Attributed interface, or by other methods used on other objects in the same area of code.

In general, new exception types should be created when programmers will want to handle one kind of error differently from another kind. Programmers can then use the exception type to execute the correct code rather than examine the contents of the exception to determine whether they really care about the exception or they have caught an irrelevant exception by accident.

The throw Statement

You throw exceptions using the throw statement:

throw expression;

where expression must evaluate to a value or variable that is assignable to Throwable - or in simple terms, a reference to a Throwable object. For example, here is an addition to AttributedImpl from earlier considerations that implements replaceValue:

public void replaceValue (String name, Object newValue) throws NoSuchAttributeException { Attr attr = find(name); // look up the attr if (attr==null) // it isn't found throw new NoSuchAttributeException(name); attr.setValue(newValue); }

The replaceValue method first looks up the current Attr object for the name. If there isn't one, it throws a new object of type NoSuchAttributeException, providing the constructor with the attribute name. Exceptions are objects, so they must be created before being thrown. If the attribute does exist, its value is replaced with the new value. You can also generate an exception by invoking a method that itself throws an exception.

When an exception is thrown, the statement or expression that caused the exception is said to complete abruptly. This abrupt completion of statements causes the call chain to gradually unwind as each block or method invocation completes abruptly until the exception is caught.

Once an exception occurs, actions after the point at which the exception occurred do not take place. The next action to occur will be in either a finally block, or a catch block that catches the exception.

The throws Clause

The definition of the replaceValue method declares which checked exceptions it throws. The language requires such a declaration because programmers invoking a method need to know the exceptions it can throw just as much as they need to know its normal behavior. The checked exceptions that a method throws are as important as the type of value it returns. Both must be declared.

The checked exceptions a method can throw are declared with a throws clause, which declares a comma-separated list of exception types. Only those exceptions that are not caught within the method must be listed.

You can throw checked exceptions that are extensions of any type of exception in the throws clause because you can use a class polymorphically anywhere its superclass is expected. A method can throw many classes of checked exceptionsall of them extensions of a particular exception classand declare only the superclass in the throws clause. By doing so, however, you hide potentially useful information from programmers invoking the method: they won't know which of the possible extended exception types could be thrown. For documentation purposes, the throws clause should be as complete and specific as possible.

The contract defined by the throws clause is strictly enforced - you can throw only a type of checked exception that has been declared in the throws clause. Throwing any other type of checked exception is invalid, whether you use throw directly or use it indirectly by invoking another method. If a method has no throws clause, that does not mean that any exceptions can be thrown: it means that no checked exceptions can be thrown.

All the standard runtime exceptions (such as ClassCastException and ArithmeticException) are extensions of the RuntimeException class. The more serious errors are signaled by exceptions that are extensions of Error, and these exceptions can occur at any time in any code. RuntimeException and Error are the only exceptions you do not need to list in your throws clauses. They are ubiquitous, and every method can potentially throw them. This is why they are unchecked by the compiler.

Because checked exceptions must be declared in a throws clause, it follows that any code fragment outside a method, or constructor, with a throws clause cannot throw a checked exception. This means that static initializers and static initialization blocks cannot throw checked exceptions, either directly or by invoking a method that throws such an exception. Non-static initializers and non-static initialization blocks are considered to be part of the constructor for a class, and so they are allowed to throw checked exceptions only if all the constructors of the class declare those checked exceptions.

You should be explicit in your throws clause, listing all the exceptions you know that you throw, even when you could encompass several exceptions under some superclass they all share. This is good self-documentation. Deciding how explicit you should be requires some thought. If you are designing a general interface or superclass you have to think about how restrictive you want to be to the implementing classes. It may be quite reasonable to define a general exception you use in the interface's throws clause, and to expect that the implementing classes will be more specific where possible. This tactic is used by the java.io package, which defines a general IOException type for its methods to throw. This lets implementing classes throw exceptions specific to whatever kind of I/O is being done.

When you override an inherited method, or implement an inherited abstract method, the throws clause of the overriding method must be compatible with the throws clause of the inherited method (whether abstract or not).

The simple rule is that an overriding or implementing method is not allowed to declare more checked exceptions in the throws clause than the inherited method does. The reason for this rule is quite simple: code written to deal with the original method declaration won't be prepared to catch any additional checked exceptions and so no such exceptions are allowed to be thrown. Subtypes of the declared exceptions can be thrown because they will be caught in a catch block for their supertype. If the overriding or implementing method does not throw a checked exception then it need not redeclare that exception.

The try-catch-finally Statement

You catch exceptions by enclosing code in try blocks. The basic syntax for a try block is:

try { statements } catch (exception_type1 identifier1) { statements } catch (exception_type2 identifier2) { statements } ... finally { statements }

where either at least one catch clause, or the finally clause, must be present. The body of the try statement is executed until either an exception is thrown or the body finishes successfully. If an exception is thrown, each catch clause is examined in turn, from first to last, to see whether the type of the exception object is assignable to the type declared in the catch. When an assignable catch clause is found, its block is executed with its identifier set to reference the exception object. No other catch clause will be executed. Any number of catch clauses, including zero, can be associated with a particular try as long as each clause catches a different type of exception. If no appropriate catch is found, the exception percolates out of the try statement into any outer try that might have a catch clause to handle it.

If a finally clause is present with a try, its code is executed after all other processing in the try is complete. This happens no matter how completion was achieved, whether normally, through an exception, or through a control flow statement such as return or break.

This example code is prepared to handle one of the exceptions replaceValue throws:

Object value = new Integer(8); try { attributedObj.replaceValue("Age", value); } catch (NoSuchAttributeException e) { // shouldn't happen, but recover if it does Attr attr = new Attr(e.attrName, value); attributedObj.add(attr); }

The try sets up a statement (which must be a block) that does something that is normally expected to succeed. If everything succeeds, the block is finished. If any exception is thrown during execution of the code in the try block, either directly via a throw or indirectly by a method invoked inside it, execution of the code inside the try stops, and the attached catch clause is examined to see whether it wants to catch the exception that was thrown.

A catch clause is somewhat like an embedded method that has one parameter namely, the exception to be caught. As with method parameters, the exception "parameter" can be declared final, or can have annotations applied. Inside a catch clause, you can attempt to recover from the exception, or you can clean up and rethrow the exception so that any code calling yours also has a chance to catch it. Or a catch can do what it needs to and then fall out the bottom, in which case control flows to the statement after the try statement (after executing the finally clause, if there is one).

A general catch clause - one that catches exceptions of type Exception, for exampleis usually a poor implementation choice since it will catch any exception, not just the specific one you are interested in. Had we used such a clause in our code, it could have ended up handling, for example, a ClassCastException as if it were a missing attribute problem.

You cannot put a superclass catch clause before a catch of one of its subclasses. The catch clauses are examined in order, so a catch that picked up one exception type before a catch for an extended type of exception would be a mistake. The first clause would always catch the exception, and the second clause would never be reached.

Only one exception is handled by any single encounter with a try clause. If a catch or finally clause throws another exception, the catch clauses of the try are not reexamined. The catch and finally clauses are outside the protection of the try clause itself. Such exceptions can, of course, be handled by any encompassing try block in which the inner catch or finally clauses were nested.

The finally clause of a try statement provides a mechanism for executing a section of code whether or not an exception is thrown. Usually, the finally clause is used to clean up internal state or to release non-object resources, such as open files stored in local variables.

Assertions

References
  • K.Arnold, J.Gosling, D.Holmes: The Java programming language. Fourth edition. Section 12: exceptions and assertions.