This article was written with Java 6 in mind.
Updated 2008/12/16.
/**
|
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.
OutputStream.close(): 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.
Sections
- 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:
if both write
and 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 DefaultDemo
below to see the problem in action.
public class DefaultDemo {
|
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 catch
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 close
as BadDemo
demonstrates.
public class BadDemo {
|
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.out
s, 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
error.
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,
BufferedOutputStream
(inheriting from FilterOutputStream) and
CipherOutputStream
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.
public class FirstExceptionDemo {
|
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.
public class FirstExceptionDemo2 {
|
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
try block.
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,
the 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)
Calling close
twice should be a safe operation -
stream wrappers
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.
public class CompositeIOException extends IOException {
|
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.
public abstract class IOTaskHandler {
|
All an implementer has to do is wrap I/O calls in the doCallback()
method
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.
public class AllExceptionsDemo extends IOTaskHandler {
|
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 RuntimeException
s.
Catching all types of Exception
or 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
IOHandler
described in Getting All The Exceptions.
Perhaps the automatic resource clean up proposal for Java 7 will alleviate the situation.
Source Code
All the sources are available in a public Subversion repository.
Repository: http://illegalargumentexception.googlecode.com/svn/trunk/code/java/
License: MIT
Project: StreamExceptionHandlingDemos
Thanks for the excellent discussion and code. Here's one more alternative that always gets the first exception and calls close only once (unfortunately, I don't know how to make the code well formatted in a comment):
ReplyDeletepublic void sendToStream(byte[] data) throws IOException {
OutputStream stream = openStream();
try {
stream.write(data);
} catch (IOException e) {
closeSilently(stream);
throw e;
}
stream.close();
}
Oops, the above doesn't handle unchecked exceptions in write.
ReplyDeleteThanks, this a great analysis of the problem.
ReplyDeleteBut, would it be fair to offer this case as proof of superiority of RAII-capable languages?
Since some days I am working on a Java project, and while striving for the most perfect solution, of course I coded something similar to your BadDemo. Leaking resources seems to be a huge problem in java, does this explain the typical recurrent issues I have with various java software (locked files, open pipes, sockets,...) ?
With RAII it would all be so much simpler, but anyway, I am bound to Java 6 in my project.
So, thanks a lot for your article.
I can only speculate on the problems your application is experiencing, but bad resource cleanup is a possibility. Consider using FindBugs to help you detect potential problems.
ReplyDeleteRAII is nice though care is still required with destructors and exception handling.
Java 7 introduces try-with-resources to make deallocation neater. The precise behaviour is described in the Java Language Specification.