Aug
27
2009
Exception programming in Java
In the past, Java programmers were actually encouraged to have deeply-nested if statements to handle business-logic errors. According to Robert C. Martin in his 1997 essay, “Java and C++ A critical comparison,” this was because during the early development of the Java language, Sun modeled Java’s Exception mechanism heavily against C++. Since then, Sun has come up with two different categorizations of Exceptions: checked and unchecked.
Today, Java programmers today know the difference between “checked” and “unchecked” Exceptions, which is fairly straightforward. The real trick is knowing how to properly take advantage of the differences in your code.
Unchecked Exceptions: This type of Exception is represented as a RuntimeException within the Java language and are not required to be declared in the throws clause of the method. These types of exceptions indicate a problem that should not routinely happen (ie. IllegalArguementException) and are unrelated to business logic errors.
Checked Exceptions: A checked Exception is treated differently than an unchecked Exception by the compiler and virtual machine. This type of Exception must be caught in the body of the method or declared in the throws clause of the method declaration. This allows the code to clearly identify the problems that may occur within a method and require the user of that method to handle them appropriately.
The key here to take advantage of the differences is to avoid superfluous Exception handling code for better maintainability and robustness. Let’s consider the following method signature as an example:
public void reflectionMethod()
throws NoSuchMethodException, IllegalAccessException, MyCustomMethod {
The only Exception that really matters here is MyCustomMethod, because it indicates a violation of a business rule. If any of the other Exceptions are thrown, a programming error has occurred. We could have declared our reflectionMethod() method to throw Exception or as Throwable. But that’s a bad idea since that would allow any kind of Exception to be thrown. Subsequently, its calling method is now obligated to rethrow these exceptions or deal with them. This quickly propagates in your code until practically every method is declared with throws Exception and could potentially hide legitimate Exceptions such as NullPointerException and create a HUGE maintenance nightmare.
The best way to deal with this is to filter the exceptions so that you don’t have to declare them in the throws clause, but still maintain differentiated exceptions that can propagate down the call stack. This is where the unchecked exception (RuntimeException subclasses) come in handy. Consider the revised code:
public void reflectionMethod() throws MyCustomMethod {
try {
// some code
}
catch(NoSuchMethodException e) {
throw new RuntimeException(e);
}
catch(IllegalAccessException e) {
throw new RuntimeException(e);
}
}
With this re-written code, you now make use of Java’s chained exception facility, allowing you to know when one exception causes another — very helpful when looking at a stack trace. This code now also allows you to filter the exceptions that would indicate programming problems and pass them on without declaring them in the throws clause.
OK, if Exceptions are so straightforward, then what is the problem? The problem is that sometimes even the best programmers do not properly implement Exceptions and this can lead to many hard to track down bugs. In past projects, this has accounted for a great deal of my debugging time, so I know the pain. Here is an example:
Improper Exception usage – a JDBC example. This type of problem is very hard to debug and can lead to hours of frustration. Here’s why. Let’s say you’ve got a JDBC-based GUI application. It runs some SQL against a data source and stores the data in a cache to be used by the GUI. Here’s the code:
package com.shepherdinteractive.exceptions;
public class ExceptionalTraps implements TableModel {
private ArrayList data; // Data cache for GUI
private String sql;
public int loadData(final Connection conn, final String sql) throws SQLException {
int result = 0;
Statement stmt = null;
ResultSet rs = null;
Object[] record = null;
this.data = new ArrayList();
try {
this.sql = sql;
stmt = conn.createStatement();
rs = stmt.executeQuery(sql);
int i = 0;
int columnCount = rs.getMetaData().getColumnCount();
while(rs.next()) {
record = new Object[columnCount];
for(i = 0; i < columnCount; i++) {
record[i] = rs.getObject(i);
}
data.add(record);
}
}
finally {
if(rs != null) {
rs.close();
}
if(stmt != null) {
stmt.close();
}
}
return result;
}
}
The main problem here is that the cache gets filled on each iteration of the RecordSet. But each of these iterations could potentially cause a checked exception, leaving the cache partially filled at the time of the exception; an unstable state. This could have repercussions throughout the application as other Objects, such as listeners, throw Exceptions which would then cause a massive chain reaction of Exceptions that eventually crash the application.
The user, not knowing anything about databases or exceptions, fills out a bug report that says, “Using the ‘MyWidget’ menu item sometimes causes the application to crash.” Once you receive this bug report, you first try to replicate the error to figure out what is wrong with the method. However, you don’t get the transaction problem the user did, so you can’t reproduce the bug. You now have a transient bug that will be extremely difficult to find, as there could be thousands of lines of code between the invocation of the menu item and this method.
Fortunately, the solution is easy, if you know what to look out for. In this case, we re-write the code so that you first try to do all of the operations that could cause an exception prior to setting the instance variables. Here is the re-written code:
ArrayList tempCache = new ArrayList();
try {
// some code...
while (rs.next( )) {
record = new Object[columnCount];
for (i = 0; i < columnCount; i++) {
record[i] = rs.getObject(i);
}
tempCache.add(record);
}
}
finally {
// some code...
}
this.data = temp;
this.sql = sql;
return result;
By modifying the code this way, the data can never become corrupted. If an exception is thrown in the method body, the virtual machine will execute the finally block and exit the method, return control to the caller, the instance members won’t be set (they will be in a valid state.)
Leave a Reply
You must be logged in to post a comment.