Try-Catch-Resource and the Exception.getSuppressed() Method
Java 7
is out there for such a long time now so this post is even
not
fashionably late but still there is one aspect of the try-with-resource
construct which, in my opinion, is sometimes overlooked: In this post I
would like to
point
out some behavioral change related to exception suppression which
might affect code migrating from
older Java versions.
The invocation of os.write() throws IOWriteException which passed the control to the finally block. The os variable is not null so the os.close() method is invoked resulting in the IOCloseException. At that point the method execution ends throwing an instance of IOCloseException. A caller similar to the one below will print 'Failed to closed stream'.
Indeed more elegant and a very easy conversion of the code - however there is an important nuance from the caller point of view: the try-catch-resource version of the method throws IOWriteException rather than IOCloseException. Obviously the caller illustrated few paragraphs above will change its behavior when using the migrated version of the method. So what had seemed to be a naive internal method change becomes now a contract change.
Not the most elegant solution I could think about - but still one of those things Java developers must be aware of.
[some sample code for try-with-resource is at https://github.com/eyal-lupu/eyallupu-blog/tree/master/Java-SE/src/test/java/com/eyalllupu/blog/java7/trywithresource]
Old Code
Let's assume the following pre-Java 7 code, for the example I assume a dummy OutputStream ('SampleStream') which throws IOWriteException when write() is invoked and IOCloseException when close() is invoked (both are faked exceptions derive from IOException). For the sake of the example both exceptions are always thrown - regardless of the state of the application/stream. Which exception would be thrown by the method below?public void testTraditionalTryCatchWithExceptionOnFinally() throws IOException { OutputStream os = null; try { os = new SampleStream(); os.write(0); // Throws IOWriteException } finally { if (os != null) { os.close(); // Throws IOCloseException } }
The invocation of os.write() throws IOWriteException which passed the control to the finally block. The os variable is not null so the os.close() method is invoked resulting in the IOCloseException. At that point the method execution ends throwing an instance of IOCloseException. A caller similar to the one below will print 'Failed to closed stream'.
try {
testTraditionalTryCatchWithExceptionOnFinally();
} catch (IOWriteException e) {
System.out.println("Failed to write to stream");
} catch (IOCloseException e) {
System.out.println("Failed to close stream");
} catch (IOException e) {
System.out.println("Error working with the stream");
}
Migrating to Try-catch-resource
The method above using a try with resource block would be written aspublic void testJava7TryCatchWithExceptionOnFinally() throws IOException {
try (OutputStream os = new SampleStream(true)) {
os.write(0); // Throws IOWriteException
}
}
Indeed more elegant and a very easy conversion of the code - however there is an important nuance from the caller point of view: the try-catch-resource version of the method throws IOWriteException rather than IOCloseException. Obviously the caller illustrated few paragraphs above will change its behavior when using the migrated version of the method. So what had seemed to be a naive internal method change becomes now a contract change.
What can be done: document and getSuppressed()
First thing is to be aware of that change - and to consider if this internal change in the method does worth it. If it is decided to do the change we should track down all usages of the modified method and to see if any changes need to be done. However in real life this is not always easy: in large projects or if the modified method is a part of a library provided to other projects (so there is no visibility on who is using it) we should also include an explicit notification of that behavior change in the library documentation/release notes. Once the change is communicated with other developers they can use the getSuppressed() method to drill down into the different use cases. The try-catch-resource block does expose the suppressed exception using the new (since Java 1.7) getSuppressed() method. This method returns all of the suppressed exceptions by the try-catch-resource block (notice that it returns ALL of the suppressed exceptions if more than one occurred). A caller might use the following structure to reconcile with existing behaviortry { testJava7TryCatchWithExceptionOnFinally(); // This is the method illustrated above } catch (IOException e) { Throwable[] suppressed = e.getSuppressed(); for (Throwable t : suppressed) { // Check T's type and decide on action to be taken } }
Not the most elegant solution I could think about - but still one of those things Java developers must be aware of.
And one last thing
Just another example on the importance of good unit test coverage which can promptly identify that kind of 'hidden changes'.[some sample code for try-with-resource is at https://github.com/eyal-lupu/eyallupu-blog/tree/master/Java-SE/src/test/java/com/eyalllupu/blog/java7/trywithresource]
Comments