At the ACCU Spring conference last year I took my exceptionsafety pattern language [Griffiths40] and redrafted the discussion of C++ idioms in Java. This wasn't as simple as I had hoped, many of the C++ techniques used have no equivalent in Java. The resulting presentation met the goal of relating the pattern language to Java and identified the necessary coding techniques. However, as Jon Jagger commented, "it lacks the same sense of resolution" that the C++ paper had.
The problem was that it raised a number of unanswered issues regarding the use of Java exceptions. During the course of the presentation these were discussed (with a lot of input from the audience) without reaching a solution. The only clear conclusion I could reach was that these were real problems that people were experiencing.
A year has passed and now I have tried to address the issue of how Java exceptions should be used in the companion article "Exceptional Java" [Griffiths48] and it is time to returned to the original theme: that of writing exception safe code.
These two articles can be read separately, but each raises issues that are addressed by the other. One final point, although I arrived at the ideas presented in "More Exceptional Java" a year before writing "Exceptional Java" I would recommend reading it second.
To give you some orientation in the exception-safety landscape I will first describe the principle landmarks: the heights of exception safety. These are the goals that we will be seeking during the discussion of implementation techniques, so if you are prone to asking, "why are we doing this?" it will be worth assuring yourself that you understand them first.
Let us begin with a program in which an exception is thrown and consider the call stack: a method a has called another method b, b has called c, and so on, until we reach x; x encounters a problem and throws an exception. This exception causes the stack to unwind, executing the code in finally (or catch) blocks, until the exception is caught and handled by a.
I'm sure that the author of x has a perfectly good reason for throwing an exception (running out of memory, disk storage, or whatever) and that the author of a knows just what to do about it (display: "Sorry, please upgrade your computer and try again!").
But exception safety is not about writing code that throws an exception (method x) nor about writing code that handles it (method a).
Exception safety is about writing the code that lies between - writing all the intervening methods in a way that ensures that something sensible happens when an exception propagates. It is about writing the typical method m in the middle of the call stack. If we want m to be "exception safe" then how should it behave?
Consider the options: If m completes its task by some other means (by using a different algorithm, or returning a "failed" status code) then it would be handling the exception. That isn't what we are concerned with - we are assuming the exception won't be handled until we reach a. Since m doesn't handle the exception we might reasonably expect that:
-
m doesn't complete its task.
-
If m has opened a file, acquired a database connection, or, more generally; if m has "allocated a resource" then the resource should not leak. The file must be closed, the database connection must be released, etc.
-
If m changes a data structure, then that structure should remain useable.
In summary: if m updates the system state, then the state must remain usable. Note that isn't quite the same as correct, for example, part of a name-and-address object may have been changed leaving mismatched name and address values.
These conditions are called the basic exception safety guarantee. Take a good look at it so that you'll recognise it later.
If you are new to this territory then the basic exception safety guarantee may seem daunting. But not only will we reach this in our travels, we will be reaching an even higher peak called the strong exception safety guarantee that places a more demanding constraint on m:
-
If m terminates by propagating an exception then it has made no change to the state of the program.
The basic and strong exception safety guarantees were first described by Dave Abrahams [Abrahams] to document an implementation of the C++ standard library.
Note that it is impossible to implement m to deliver either the basic or strong exception safety guarantees if the behaviour in the presence of exceptions of the methods it calls isn't known. This is particularly relevant when the client of m (that is l) supplies the methods to be called either as callbacks or as implementations of virtual member methods. In such cases the only recourse is to document the constraints on them.
If we assume a design with fully encapsulated data then each method need only be held directly responsible for aspects of the object of which it is a member. For the rest, the code in each method must rely on the methods it calls to behave as documented. (We have to rely on documentation in this case, since in Java, as in C++, there is no way to express these constraints in the code.)
As we proceed we'll find frequent outcrops of the rock that supports our mountains, which is named after the area in which it is found and is called the no-throw exception safety guarantee, as the name suggests this implies that the corresponding function will never propagate an exception. Clearly operations on the fundamental types provide this guarantee, as will any sequence of no-throw operations. If it were not for the firm footing that these outcrops of the no-throw exception safety guarantee provide we would have great difficulty ascending the heights. Although this was known and used in earlier work I think it was first made explicit and named by Herb Sutter [Sutter].
In Java objects are in a very real sense immortal - they do not die - and are merely forgotten when there are no references to them. This has a number of obvious advantages - in particular, one cannot have a reference to an object that no longer exists (this is a major element of the Java security model).
However, the basic and strong exception guarantees both refer to "the system state" and it may not be obvious whether objects awaiting garbage collection should be considered part of this. These 'forgotten' objects cannot be accessed and I propose to ignore them, they are not part of the system state.
Java has both checked and unchecked exceptions and the companion article "Exceptional Java" examines the consequences of this. For the purposes of this article I claim that both checked and unchecked exceptions must be considered when reviewing code for exception safety. A method that doesn't catch an exception doesn't care about the type of the exception, particularly whether it is checked or unchecked, so throws clauses are of little account when reasoning about exception safety.
Why must unchecked exceptions be considered? Because they can be thrown by x and caught by a. And the code that catches them requires guarantees about the state of the system. If, for example, it is going to restart the subsystem that encountered the problem, then it needs to know that the subsystem died in an orderly manner.
Throughout this article whenever I mention exceptions without qualification I mean it in the inclusive sense, "either checked or unchecked exception".
Not considering unchecked exceptions is a frequent cause of errors. Two factors contributing to this are: a tendency for developers to rely on the compiler to indicate any failure to consider exceptions, and that error handling is often omitted from demonstrative code - as it obscures the point. Consider the following example (widely quoted as a way to avoid flicker when using the AWT):
public void repaint() { Graphics g = getGraphics(); paint(g); g.dispose(); }
It is possible for an unchecked exception to propagate from paint(g), which would bypass the g.dispose() statement. It is important that this doesn't happen in a real application as it releases system resources. It is not sufficient to assume that the finalize method of the object formerly known as g will release the resources as there is no guarantee that the finalize method is called in a timely manner, or indeed ever.
In Java the only way to guarantee that a method on an object is executed is to call it before the object is forgotten. In code written for a production environment I'd expect to see:
public void repaint() { Graphics g = getGraphics(); if (null != g) { try { paint(g); } finally { g.dispose(); } } }
In this code fragment it should be obvious that the resources will be released whether or not paint() propagates an exception - look at the three significant steps: allocate, use, release and the logic that binds them together.
While on the subject of finally, please use it idiomatically: while it is legal to exit from a finally-block using return, continue or break doing so defies the 'principle of least surprise'. The next programmer to work on the code (it may be you) will expect that the exception continues to propagate. If the exception may be handled say so: by using catch!
While repeatedly reproducing the same code in slightly different contexts may be good for productivity by metrics such as lines-of-code it is tedious and a possible source of errors. Wherever paired operations (such as allocation and release) need to surround a piece of code it may be useful to employ the EXECUTE-AROUND-METHOD idiom [Henney]. This exists in a number of languages and in Java it is expressed by passing an anonymous local class to a method that allocates the resource, passes it to a method in the supplied code, and finally release the resource:
public class ExecuteAroundMethod { private ListSelectionModel sel; private interface Adjustment { public void use( ListSelectionModel sel); } private void executeAround( Adjustment adjustment) { sel.setValueIsAdjusting(true); // 'allocate' try { adjustment.use(sel); // 'use' } finally { sel.setValueIsAdjusting(false); // 'release' } } public void removeOdd() { executeAround(new Adjustment() { public void use( ListSelectionModel s) { for (int i = entries; -i != 0;) if (0 != i % 2) s.removeIndexInterval(i, i); } }); } }
There is a trade-off here, we've ensured that there is only one method to examine to ensure that paired operations always occur correctly, but we've paid a price by introducing extra classes and method calls.
We are now going to look at strongly exception safe version of a simple method. The following example is a translation from C++ of an example introduced as a test case by Tom Cargill. The method we will examine, copy(), is one that aims to copy the state of source to this:
public class Whole { private static class PartOne implements Cloneable { /* omitted */ } private static class PartTwo implements Cloneable { /* omitted */ } private PartOne p1; private PartTwo p2; public void copy(Whole source) { /* What goes here? */ } }
A (very naïve) implementation might be:
public void copy(Whole source) { p1 = rhs.p1.clone(); p2 = rhs.p2.clone(); }
Is this exception safe?
If we make the reasonable assumption that the clone() methods are themselves strongly exception safe then copy() supports the basic guarantee. (This is also the case if the clone() methods support the basic guarantee.) If the clone() methods are not exception safe there is little that copy() can do to achieve exception safety.
Only if the PartTwo.clone() method called in the second line won't throw an exception can this version of copy() support the strong guarantee (after modifying p1 the system state has definitely changed). Of course, the nothrow guarantee is an unreasonable expectation of a clone() method, an 'out of memory' exception is a possibility for any reasonable implementation. On the other hand there is nothing to indicate that modifying p1 alone will make the object unusable so we can claim to meet the basic guarantee.
Now consider a slightly updated version that addresses the problem of an exception being thrown by the second clone() call:
public void copy(Whole source) { PartOne temp = rhs.p1.clone(); p2 = rhs.p2.clone(); p1 = temp; }
With the presumption that cloning does nothing that needs reversing then an exception propagating from either of the first two lines permits any changes to the system to be forgotten on exit from copy(). This is the strong guarantee.
In the above we've assumed that cloning doesn't do anything that needs to be undone. This isn't always true - for example PartOne and PartTwo may own a resource that needs releasing. Adding the complexity of ensuring dispose() methods are called and generalise slightly (I wouldn't normally expect to need the checks against null for clone() but other methods used in this context might):
public void copy(Whole source) { PartOne t1 = rhs.p1.clone(); if (null != t1) { try { PartTwo t2 = rhs.p2.clone(); if (null != t2) { try { // examples of methods // that might throw t1.setParent(this); t2.setParent(this); // ******************************** // This is the pivotal point of the // code - everything that could // fail is before this point. // Nothing that makes persistent // changes to the state of the // system is before this point. // ******************************** // The following commits the // change to the system state. // Importantly it won't throw. PartOne swap1 = t1; t1 = p1; p1 = swap1; PartTwo swap2 = t2; t2 = p2; p2 = swap2; } finally { // either frees the original // resources or of the // temporary - depending // whether we passed the // pivot uneventfully. t2.dispose(); } } } finally { // either frees the original // resources or of the temporary // - depending whether we // passed the pivot uneventfully. t1.dispose(); } } }
The code is structured in such a way that for each object creation that succeed the dispose() method will be invoked. It will be invoked on either the original instance if no exception is propagated or the replacement instance in the case of an exception. The only assumptions needed to demonstrate the strong guarantee being attained are: no exceptions are propagated by the dispose() calls and that the setParent() calls are themselves exception safe.
This code structure once again shows the ALLOCATE-USERELEASE idiom we observed earlier with a subtle variation, if no error occurs then it's a different resource that is released. This has been described before in a C++ context [Henney21] and goes by the name ALLOCATE-BEFORE-RELEASE. (Strictly, the original reference refers to COPY-BEFORE-RELEASE, a special case equivalent to the current example.)
A close examination of the above example should make it clear that when committing updates to the system state we need operations that don't throw exceptions. The assignments of references used to swap the new state into the object are obviously safe - they are guaranteed not to throw by the language, but if either call to dispose() were to throw an exception then the strong guarantee would be violated.
This is an area in which Java standard library documentation is deficient, as it only addresses checked exceptions. Consider the method Graphics.dispose() used in the first example: could this propagate an unchecked 'null pointer' exception? I hope not - but without documentation of this point we don't know.
On terrestrial mountains above a certain height there is a "death zone" where the supply of oxygen is insufficient to support life for long periods. Something similar happens with exception safety: there is a cost to implementing the strong exception safety guarantee. The technique illustrated above can involve the creation of extensive duplicate data structure: the additional objects created and the resources they allocate can be expensive in both space and time. If repeated at all the levels of our call stack the costs of doing this can suffocate an application.
The alternative to changing a copy of an object is to change the original and either accept that an exception could leave a series of changes incomplete. The result is that either the system will be in an unknown state or we must be prepared to back out changes. For either approach what we need to know is that nothing will go horribly wrong, the basic exception safety guarantee.
To provide an example I'm going to elaborate on the previous example by extending the class and adding a (potentially) large container to the derived class:
public class BiggerWhole extends Whole { private Properties parameters = new Properties(); public void copy(BiggerWhole source) { /* What goes here? */ } }
Using the techniques we examined earlier we would take a clone of source.parameters then call super.copy(), and finally update parameters. If we decide that the cost of creating a copy of parameters is unacceptable then we can update it with the understanding that if an exception occurs then we make no promise to the client code of the exact state of BiggerWhole, the client code must take appropriate action. Vis:
public void copy(BiggerWhole source) { super.copy(source); parameters.clear(); Enumeration e = source.parameters.propertyNames(); while (e.hasMoreElements()) { String key = (String)e.nextElement(); parameters.put(key, source.parameters.getProperty(key)); } }
At any point after super.copy() returns then the state of the system has changed. However an exception is still possible. However, if failing to update parameters completely leaves the object in a sensible state then this may be acceptable.
The exception safety landmarks are useful in Java, as they are in C++. The basic, strong and nothrow guarantees clearly make sense and can be applied when writing or reviewing code. There are techniques for writing code to these guarantees and these have been demonstrated. While I must agree with those C++ developers who consider these techniques less elegant than those available in C++ I see the principal issue to be the failure of a significant part of the Java community to believe that the problem they solve exists.
Ignoring a problem does not make it go away and as unchecked exceptions can encapsulate rare but plausible events (e.g. out of memory) and even exceptions explicitly thrown by the programmer it is unreasonable to ignore them. It is unfortunate that they also include that shouldn't happen in a correct program.
The path that led Java to the current handling of unchecked exceptions is paved with good intentions: rather than the JVM having undefined behaviour when "bad things" happen the behaviour is defined. But this has only shifted the problem, because the programmer is not working with the raw JVM, but with library code that doesn't fully document its behaviour.
In addition to unchecked exceptions being undocumented, there are no compile-time tools for verifying exception safety. This once again leaves the problem with the developers, who must document and check the requirements for themselves.
[Abrahams] Dave Abrahams, Exception Safety in STLPort, http://www.stlport.org/doc/exception_safety.html
[Henney] Kevlin Henney, Java Patterns and Implementations, http://homepages.tesco.net/~jophran/UKPatterns/plunk1/JavaPatterns.html
Overload Journal #49 - Jun 2002 + Programming Topics
Browse in : |
All
> Journals
> Overload
> 49
(8)
All > Topics > Programming (877) Any of these categories - All of these categories |