This article was written with Java 6 in mind.
If you aren't careful with streams in Java, you end up with resource leaks. This article addresses some of the pitfalls of stream handling.
The examples that follow use implementations of OutputStream. Which output stream is not important - implementation details are encapsulated by the object and may change between Java versions. It is the developer's responsibility to honour the contract as per the documentation.
Closes this output stream and releases any system resources associated with this stream. The general contract of close is that it closes the output stream. A closed stream cannot perform output operations and cannot be reopened.
New Java developers pick up pretty quickly that they have to close streams. Problems arise when they start thinking about how to do it.
- Imperfect, But A Good Default
- How Not To Close A Stream
- How To Get The First Exception
- How To Get The First Exception (Round 2)
- When One Exception Is Not Enough
- Getting All The Exceptions
- What Is The Best Way?
- Source Code
Imperfect, But A Good Default
The above code is a good default: the method always informs the caller that
an exception has been raised and always closes the stream. It has one problem:
close throw exceptions, the
second exception will be passed up the stack even though it is likely that
the first was the root cause. Compile and run
below to see the problem in action.
Output from DefaultDemo:
Writing: 1 Throwing Exception Stream closed Throwing Exception ERROR java.io.IOException at DefaultDemo$1.close(DefaultDemo.java:23) at DefaultDemo.sendToStream(DefaultDemo.java:36) at DefaultDemo.main(DefaultDemo.java:44)
The exception received by the caller is the one from
close and it is not apparent from the stack trace that
write threw an exception.
The exception from
close is likely to be related to the one from
write, so this may not be a big problem.
How Not To Close A Stream
A common "solution" to the problem is seen in this code.
(Inexperienced programmers will often try to fit another
block in there too.)
The idea behind this approach is that if
write throws an exception,
the stream will be closed and the exception will not be hidden.
Unfortunately, this method completely ignores exceptions from
Output from BadDemo:
Writing: 1 Stream closed Throwing Exception OK
An exception was thrown by
close, but the program doesn't detect an error.
If it wasn't for the
System.outs, we wouldn't know an exception had been thrown.
Some developers cotton on to this an put some error logging in the catch block
- this notifies you (if you read the logs), but your program is still not dealing with the
You might be tempted to think that the only reason
close would throw
and exception would be because the resource couldn't be released - unfortunate, but not
your problem. This would be a mistake - plenty of streams write data in their close blocks,
(inheriting from FilterOutputStream) and
being two documented examples. If you don't handle
close properly, you might commit incomplete data
to the stream and not know it.
How To Get The First Exception
It is possible to get the first exception, though the following code is inelegant.
Output from FirstExceptionDemo:
Writing: 1 Throwing Exception Stream closed Throwing Exception ERROR java.io.IOException at FirstExceptionDemo$1.write(FirstExceptionDemo.java:15) at java.io.OutputStream.write(Unknown Source) at java.io.OutputStream.write(Unknown Source) at FirstExceptionDemo.sendToStream(FirstExceptionDemo.java:35) at FirstExceptionDemo.main(FirstExceptionDemo.java:57)
This code gives us what is probably the primary cause of the errors, but if we added an input stream the code would get difficult to follow and unpleasant to maintain.
How To Get The First Exception (Round 2)
A less verbose way to get the first exception is shown below.
This code relies on being allowed to call the stream
close method twice.
If an exception is thrown by a stream operation, the exception is passed up the stack and
any exceptions from close are swallowed. If the stream operations succeed and closing the
stream throws an exception, it is not swallowed silently because close is called in the
Output from FirstExceptionDemo2:
Writing: 1 Throwing Exception Stream closed Throwing Exception ERROR java.io.IOException at streamdemos.FirstExceptionDemo2$1.write(FirstExceptionDemo2.java:47) at java.io.OutputStream.write(OutputStream.java:99) at java.io.OutputStream.write(OutputStream.java:58) at streamdemos.FirstExceptionDemo2.sendToStream(FirstExceptionDemo2.java:65) at streamdemos.FirstExceptionDemo2.main(FirstExceptionDemo2.java:84)
Here, the first exception (from
write) is printed to the console.
If the code is modified so that
write does not throw an exception,
close exception is reported:
Stream closed Throwing Exception Stream closed Throwing Exception ERROR java.io.IOException at streamdemos.FirstExceptionDemo2$1.close(FirstExceptionDemo2.java:51) at streamdemos.FirstExceptionDemo2.sendToStream(FirstExceptionDemo2.java:63) at streamdemos.FirstExceptionDemo2.main(FirstExceptionDemo2.java:81)
close twice should be a safe operation -
close their wrapped streams and the code that opens a stream is ultimately responsible for closing it.
When One Exception Is Not Enough
To get all the relevant error messages, you need to aggregate them somehow. This IOException class is an incomplete implementation (this post is long enough already) of a class that does this.
Getting All The Exceptions
To minimise the messy code used in "How To Get The First Exception", it is best to come up with a generalised solution. The I/O task handler listed below ensures that no exceptions are hidden by exceptions from Closeable objects.
All an implementer has to do is wrap I/O calls in the
and register any streams with the handler.
Streams should be registered with the handler in the call after they have been created to ensure they get closed.
The handler will take care of closing streams and dealing with exceptions.
Its usage can be seen below where IOTaskHandler is extended to implement an I/O task.
You could easily use an anonymous class instead - extending it is clearer for demonstration purposes.
Output from AllExceptionsDemo:
ERROR streamdemos.io.CompositeRuntimeException at streamdemos.io.IOTaskHandler.run(IOTaskHandler.java:73) at streamdemos.AllExceptionsDemo.main(AllExceptionsDemo.java:73) CAUSED BY: java.lang.RuntimeException: out:write - not even an IOException at streamdemos.AllExceptionsDemo$1.write(AllExceptionsDemo.java:51) at java.io.OutputStream.write(OutputStream.java:99) at java.io.OutputStream.write(OutputStream.java:58) at streamdemos.AllExceptionsDemo.doCallback(AllExceptionsDemo.java:66) at streamdemos.io.IOTaskHandler.runCallbackAndClose(IOTaskHandler.java:85) at streamdemos.io.IOTaskHandler.run(IOTaskHandler.java:69) at streamdemos.AllExceptionsDemo.main(AllExceptionsDemo.java:73) java.io.IOException: out:close at streamdemos.AllExceptionsDemo$1.close(AllExceptionsDemo.java:56) at streamdemos.io.IOTaskHandler.close(IOTaskHandler.java:104) at streamdemos.io.IOTaskHandler.runCallbackAndClose(IOTaskHandler.java:93) at streamdemos.io.IOTaskHandler.run(IOTaskHandler.java:69) at streamdemos.AllExceptionsDemo.main(AllExceptionsDemo.java:73)
As you can see in the above listing, the application reports all the exceptions encountered.
Decisions need to be made in the
IOHandler implementation, like
whether the code should handle
Catching all types of
Throwable should normally be avoided.
Distinctions need to be made between checked exceptions (which the caller may recover from)
and runtime exceptions (indicating a programming error).
Although complicated, this type of error handling provides information that is normally hidden from you.
What Is The Best Way?
So, is it worth doing all this? The answer depends on the use case. I follow this procedure:
- Use the code described in Imperfect, But A Good Default.
- If an exception hiding bug is discovered, use code like that described in How To Get The First Exception (Round 2).
- Under exceptional circumstances, use a class like the
IOHandlerdescribed in Getting All The Exceptions.
Perhaps the automatic resource clean up proposal for Java 7 will alleviate the situation.
All the sources are available in a public Subversion repository.