Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Java Tutor is my platform for teaching about Java 7+ and JavaFX 2.0+, mainly via programming projects.
Three of Java SE 7's language enhancements improve Java's exception-handling mechanism. These enhancements let you catch multiple exception types, rethrow exceptions with improved type checking, and simplify resource cleanup with the try-with-resources statement.
catch block to catch more than one type of exception. The purpose of this "multicatch" feature is to reduce code duplication and reduce the temptation to catch overly broad exceptions (catch (Exception ex), for example).
Suppose you've developed an application that gives you the flexibility to copy data to a database or file. Listing 1 presents a CopyToDatabaseOrFile class that simulates this situation, and demonstrates the problem with catch block code duplication.
// CopyToDatabaseOrFile.java
import java.io.IOException;
import java.sql.SQLException;
public class CopyToDatabaseOrFile
{
public static void main(String[] args)
{
try
{
copy();
}
catch (IOException ex)
{
System.out.println(ex.getMessage());
// additional handler code
}
catch (SQLException ex)
{
System.out.println(ex.getMessage());
// additional handler code that's identical to the previous handler's
// code
}
}
static void copy() throws IOException, SQLException
{
if (Math.random() < 0.5)
throw new IOException("cannot copy to file");
else
throw new SQLException("cannot copy to database");
}
}
Listing 1: CopyToDatabaseOrFile.java
Java SE 7 overcomes this code duplication problem by letting you specify multiple exception types in a catch block where each successive type is separated from its predecessor by placing a vertical bar (|) between these types:
try
{
copy();
}
catch (IOException | SQLException ex)
{
System.out.println(ex.getMessage());
}
Now, when copy() throws either exception, the exception will be caught and handled by the catch block.
When multiple exception types are listed in a catch block's header, the parameter is implicitly regarded as final. As a result, you cannot change the parameter's value. For example, you cannot change the reference stored in the previous code fragment's ex parameter.
Note: The bytecode resulting from compiling a catch block that handles multiple exception types will be smaller than compiling several catch blocks that each handle only one of the listed exception types. A catch block that handles multiple exception types contributes no duplicate bytecode during compilation. In other words, the bytecode doesn't contain replicated exception handlers.
|
catch block parameter (the parameter is effectively final). When an exception originates from the preceding try block and is a supertype/subtype of the parameter's type, the compiler throws the actual type of the caught exception instead of throwing the type of the parameter (as is done in previous Java versions).
The purpose of this "final rethrow" feature is to facilitate adding a try-catch statement around a block of code to intercept, process, and rethrow an exception without affecting the statically determined set of exceptions thrown from the code. Also, this feature lets you provide a common exception handler to partly handle the exception close to where it's thrown, and provide more precise handlers elsewhere that handle the rethrown exception. Consider Listing 2.
// MonitorEngine.java
class PressureException extends Exception
{
PressureException(String msg)
{
super(msg);
}
}
class TemperatureException extends Exception
{
TemperatureException(String msg)
{
super(msg);
}
}
public class MonitorEngine
{
public static void main(String[] args)
{
try
{
monitor();
}
catch (Exception e)
{
if (e instanceof PressureException)
System.out.println("correcting pressure problem");
else
System.out.println("correcting temperature problem");
}
}
static void monitor() throws Exception
{
try
{
if (Math.random() < 0.1)
throw new PressureException("pressure too high");
else
if (Math.random() > 0.9)
throw new TemperatureException("temperature too high");
else
System.out.println("all is well");
}
catch (Exception e)
{
System.out.println(e.getMessage());
throw e;
}
}
}
Listing 2: MonitorEngine.java
Listing 2 simulates the testing of an experimental rocket engine to see if the engine's pressure or temperature exceeds a safety threshold. It performs this testing via the monitor() helper method.
monitor()'s try block throws PressureException when it detects a pressure extreme, and throws TemperatureException when it detects a temperature extreme. (Because this is only a simulation, random numbers are used.) The try block is followed by a catch block designed to partly handle the exception by outputting a warning message. This exception is then rethrown so that monitor()'s calling method can finish handling the exception.
Before Java SE 7, you couldn't specify PressureException and TemperatureException in monitor()'s throws clause because the catch block's e parameter is of type java.lang.Exception and rethrowing an exception was treated as throwing the parameter's type. Starting with Java SE 7, you can specify these exception types in the throws clause because the compiler determines that the exception thrown by throw e came from the try block, and only PressureException and TemperatureException can be thrown from this block.
Because you can now specify static void monitor() throws PressureException, TemperatureException, you can provide more precise handlers where monitor() is called, as the following code fragment demonstrates:
try
{
monitor();
}
catch (PressureException pe)
{
System.out.println("correcting pressure problem");
}
catch (TemperatureException te)
{
System.out.println("correcting temperature problem");
}
Because of the improved type checking offered by final rethrow, source code that compiled under previous versions of Java might fail to compile under Java SE 7. For example, consider Listing 3.
// BreakageDemo.java
class SuperException extends Exception
{
}
class SubException1 extends SuperException
{
}
class SubException2 extends SuperException
{
}
public class BreakageDemo
{
public static void main(String[] args) throws SuperException
{
try
{
throw new SubException1();
}
catch (SuperException se)
{
try
{
throw se;
}
catch (SubException2 se2)
{
}
}
}
}
Listing 3: BreakageDemo.java
Listing 3 compiles under Java SE 6 and earlier. However, it fails to compile under Java SE 7, whose compiler detects and reports the fact that SubException2 is never thrown in the body of the corresponding try statement.
Although unlikely to occur, it's possible to run into this problem. Instead of grumbling about the breakage, consider the value in having the compiler detect a source of redundant code whose removal results in cleaner source code and a smaller classfile.
BufferedReader br = null;
try
{
FileReader fr = new FileReader("somefile.txt");
br = new BufferedReader(fr);
String line;
while ((line = br.readLine()) != null)
{
// Process line.
}
}
catch (IOException ioe)
{
System.err.println(ioe.getMessage());
}
finally
{
if (br != null)
try
{
br.close();
}
catch (IOException ioe)
{
}
}
This code fragment is hideous because of the amount of code that's necessary to ensure that the file is closed. Fortunately, Java SE 7 provides the try-with-resources statement to simplify this pattern. Using try-with-resources, you'd express the previous code fragment as follows:
try (BufferedReader br = new BufferedReader(new FileReader("somefile.txt")))
{
String line;
while ((line = br.readLine()) != null)
{
// Process line.
}
}
catch (IOException ioe)
{
System.err.println(ioe.getMessage());
}
This much simpler code fragment declares a single file resource (an object that must be closed after the program no longer needs the object) between the parentheses following try. This resource's close() method is called whether or not an exception is thrown.
Consider a more complex scenario where you want to copy one file to another. Listing 4 presents a FileCopy class whose copy() method demonstrates one approach to codifying this copy operation.
// FileCopy.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopy
{
public static void main(String[] args) throws IOException
{
if (args.length != 2)
{
System.err.println("usage: java FileCopy srcFile dstFile");
return;
}
copy(args[0], args[1]);
}
static void copy(String srcFile, String dstFile) throws IOException
{
FileInputStream fis = new FileInputStream(srcFile);
try
{
FileOutputStream fos = new FileOutputStream(dstFile);
try
{
byte[] buffer = new byte[4096];
int n;
while ((n = fis.read(buffer)) > 0)
fos.write(buffer, 0, n);
}
finally
{
fos.close();
}
}
finally
{
fis.close();
}
}
}
Listing 4: FileCopy.java version 1
The copy() method ensures that the appropriate streams are closed when an exception is thrown. Because it can be tedious to verify that the closing logic works correctly, try-with-resources lets you declare multiple resources so that this logic is carried out for you -- see Listing 5.
// FileCopy.java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileCopy
{
public static void main(String[] args) throws IOException
{
if (args.length != 2)
{
System.err.println("usage: java FileCopy srcFile dstFile");
return;
}
copy(args[0], args[1]);
}
static void copy(String srcFile, String dstFile) throws IOException
{
try (FileInputStream fis = new FileInputStream(srcFile);
FileOutputStream fos = new FileOutputStream(dstFile))
{
byte[] buffer = new byte[4096];
int n;
while ((n = fis.read(buffer)) > 0)
fos.write(buffer, 0, n);
}
}
}
Listing 5: FileCopy.java version 2
Listing 5's try-with-resources statement declares two file resources that must be closed; both resource declarations are separated by a semicolon, which is mandatory. When the copy() method ends (normally or via a thrown exception), fis's and fos's close() methods are called, but in the opposite order to which these resources were created (fis was created before fos). Hence, fos.close() is called before fis.close().
Note: A try-with-resources statement can have catch and finally blocks. These blocks are executed after all declared resources have been closed.
|
Suppose that Listing 5's fos.write(buffer, 0, n); method call throws an instance of the java.io.IOException class. Now suppose that the behind-the-scenes fos.close() method call results
in a thrown IOException instance. This latter exception is suppressed, and the exception thrown by fos.write(buffer, 0, n); is the exception thrown out of the copy() method. The suppressed exception can be retrieved by calling java.lang.Throwable's public final Throwable[] getSuppressed() method.
Note: Throwable provides a public final void addSuppressed(Throwable exception) method that is automatically and implicitly called by the try-with-resources statement in order to record an exception that is thrown from a resource's close() method and is being suppressed.
|
A resource's class implements the java.lang.AutoCloseable interface or its java.lang.Closeable subinterface. Each interface provides a close() method that performs the close operation.
Unlike Closeable's close() method, which is declared to throw only IOException (or a subtype), AutoCloseable's close() method is declared to throw Exception. As a result, classes that implement AutoCloseable, Closeable, or a subinterface can declare the close() method to throw any kind of exception. The close() method should be declared to throw a more specific exception, or (as with java.util.Scanner's close() method) to not throw an exception if the method cannot fail.
Implementations of Closeable's close() method are idempotent; subsequent calls to close() have no effect on the resource. In contrast, implementations of AutoCloseable's
close() method are not required to be idempotent, but making them idempotent is recommended.
Note: Many of Java SE 7's classes and interfaces have been retrofitted to implement or extend AutoCloseable directly or indirectly via its Closeable subinterface. These classes and interfaces include the following:
|
catch (IOException | SQLException ex), what is the type of parameter ex?
catch (IOException ex), can you modify parameter ex from within this catch
block?
MonitorEngine that demonstrates the rethrowing exceptions with improved type checking feature. Modify the catch block in that version's monitor() method to assign null to e just before the throw e; statement. What happens and why?
AutoCloseable, Closeable, or a subinterface? For example, what does the compiler do when it encounters try (Object o = new Object()) {}?
You can download this post's code and answers here. Code was developed and tested with JDK 7u2 on a Windows XP SP3 platform.
* * *
I welcome your input to this blog, and will write about relevant topics that you suggest. While waiting for the next blog post, check out my TutorTutor website to learn more about Java and other computer technologies (and that's just the beginning).