In “Effective Java“, Joshua Bloch wrote 9 tips about how to handle exceptions in Java. These tips have become the de facto standard for Java exception handling. In this post, I list some examples of Java exception handling in some open source projects and comment the usage by following the 9 tips of exception handling.
The 9 tips about Java exception handling are:
1. Use exceptions only for exceptional conditions 2. Use checked exceptions for recoverable conditions and runtime exceptions for programming errors 3. Avoid unnecessary use of checked exceptions 4. Favor the use of standard exceptions 5. Throw exceptions appropriate to the abstraction 6. Document all exceptions thrown by each method 7. Include failure-capture information in detail messages 8. Strive for failure atomicity 9. Don't ignore exceptions
1. Use exceptions only for exceptional conditions
This item is mainly about avoiding using exceptions for ordinary control flow.
For example, instead of using exception to terminate a loop control flow:
try{ Iterator<Foo> iter = ...; while(true) { Foo foo = i.next(); ... } } catch (NoSuchElementException e){ } |
the regular iteration over a collection should be used:
for(Iterator<Foo> iter = ...; i.hasNext();){ Foo foo = i.next(); ... } |
I didn’t find any examples that use exceptions for regular control flow.
2. Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
In most cases, if the caller can recover the exception, then checked exceptions should be used. If not, runtime exception should be used. Runtime exceptions indicates programming errors that can be prevented by checking some preconditions, such as array boundary and nullness checks.
In the following method, IllegalArgumentException is a RuntimeException, whose usage indicates programming errors. Programming errors can often be avoided by checking the preconditions. So this is a bad example based on this tip. The exception can be avoided by checking preconditions, i.e., “hasNext()” method here. (link to source code)
/** * Convert a tag string into a tag map. * * @param tagString a space-delimited string of key-value pairs. For example, {@code "key1=value1 key_n=value_n"} * @return a tag {@link Map} * @throws IllegalArgumentException if the tag string is corrupted. */ public static Map<String, String> parseTags(final String tagString) throws IllegalArgumentException { // delimit by whitespace or '=' Scanner scanner = new Scanner(tagString).useDelimiter("\\s+|="); Map<String, String> tagMap = new HashMap<String, String>(); try { while (scanner.hasNext()) { String tagName = scanner.next(); String tagValue = scanner.next(); tagMap.put(tagName, tagValue); } } catch (NoSuchElementException e) { // The tag string is corrupted. throw new IllegalArgumentException("Invalid tag string '" + tagString + "'"); } finally { scanner.close(); } return tagMap; } |
3. Avoid unnecessary use of checked exceptions
Checked exceptions force callers to deal with the exceptional conditions because compiler will complain if not. Overuse of checked exceptions brings the burden to callers for handling the exceptional conditions. So checked exceptions should be used when necessary. The rule of thumb for using a checked exception is when the exception can not be avoided by checking preconditions and the caller can take some useful actions for handling the exception.
The commonly used runtime exceptions themselves are examples of NOT overuse of checked exceptions. The common runtime exceptions are: ArithmeticException, ClassCastException, IllegalArgumentException, IllegalStateException, IndexOutOfBoundExceptions, NoSuchElementException, and NullPointerException.
In the following method (link to source code), when propertyName is not one of the target cases, there is not much the caller can do, so a runtime exception is thrown.
@Override public Object get(String propertyName) { switch (propertyName.hashCode()) { case 842855857: // marketDataName return marketDataName; case -1169106440: // parameterMetadata return parameterMetadata; case 106006350: // order return order; case 575402001: // currency return currency; case 564403871: // sensitivity return sensitivity; default: throw new NoSuchElementException("Unknown property: " + propertyName); } } |
4. Favor the use of standard exceptions
The most commonly reused Java exception classes are as follows. You may check out the complete list here.
1. java.io.IOException 2. java.io.FileNotFoundException 3. java.io.UnsupportedEncodingException 4. java.lang.reflect.InvocationTargetException 5. java.security.NoSuchAlgorithmException 6. java.net.MalformedURLException 7. java.text.ParseException 8. java.net.URISyntaxException 9. java.util.concurrent.ExecutionException 10. java.net.UnknownHostException
None of the top 10 is the most commonly used ones shown in the book. But note that these are counted by projects, i.e., if a class is used in a project, it’s counted only once no matter how many methods in the project is using it. So this is by # of projects, but by # of occurrences in code.
5. Throw exceptions appropriate to the abstraction
Exception thrown should have connection to the task the caller performs. This item introduce exception transaltion (catch an exception and throw another) and exception chaining (wrap an exception in a new exception to keep the causal chain of the exception).
private void serializeBillingDetails(BillingResult billingResult, BillingDetailsType billingDetails) { try { final JAXBContext context = JAXBContext .newInstance(BillingdataType.class); final ByteArrayOutputStream out = new ByteArrayOutputStream(); final Marshaller marshaller = context.createMarshaller(); marshaller.setProperty("jaxb.formatted.output", Boolean.FALSE); final BillingdataType billingdataType = new BillingdataType(); billingdataType.getBillingDetails().add(billingDetails); marshaller.marshal(factory.createBillingdata(billingdataType), out); final String xml = new String(out.toByteArray(), "UTF-8"); billingResult.setResultXML(xml.substring( xml.indexOf("<Billingdata>") + 13, xml.indexOf("</Billingdata>")).trim()); billingResult.setGrossAmount(billingDetails.getOverallCosts() .getGrossAmount()); billingResult.setNetAmount(billingDetails.getOverallCosts() .getNetAmount()); } catch (JAXBException | UnsupportedEncodingException ex) { throw new BillingRunFailed(ex); } } |
The above method catches JAXBException and UnsupportedEncodingException, and rethrows a new exception that is appropriate to the method’s abstraction level. The new BillingRunFailed exception wraps the original exception. So this is a good example of exception chaining. The benefit of exception chaining is keeping the lower-level exception that is helpful for debugging the problem. (link to source code)
6. Document all exceptions thrown by each method
This is heavily under-used. Most of the public APIs lack of the @throws Java doc to explain the exception being thrown.
Here is a good example. (link to source code)
... * * @throws MalformedURLException The formal system identifier of a * subordinate catalog cannot be turned into a valid URL. * @throws IOException Error reading subordinate catalog file. */ public String resolveSystem(String systemId) throws MalformedURLException, IOException { ... |
This is a bad example of lacking the information about in what cases the exception is thrown.
* @throws Exception exception */ public void startServer() throws Exception { if (!externalDatabaseHost) { |
7. Include failure-capture information in detail messages
private OutputStream openOutputStream(File file) throws IOException { if (file.exists()) { if (file.isDirectory()) { throw new IOException("File '" + file + "' exists but is a directory"); } if (!file.canWrite()) { throw new IOException("File '" + file + "' cannot be written to"); } } else { final File parent = file.getParentFile(); if (parent != null) { if (!parent.mkdirs() && !parent.isDirectory()) { throw new IOException("Directory '" + parent + "' could not be created"); } } } return new FileOutputStream(file, false); } |
In this method, the IOException uses different string to pass the different failure-capture information.
8. Strive for failure atomicity
Item 8 is about failing. The general rule is that a failed method should not change the state of the objects in the method. In order to fail early, one way is to check parameters for validity before performing the operation. The following is a good example of following this tip.
/** * Assigns a new int value to location index of the buffer instance. * @param index int * @param newValue int */ public void modifyEntry(int index, int newValue) { if (index < 0 || index > size - 1) { throw new IndexOutOfBoundsException(); } // ((int[]) bufferArrayList.get((int) (index / pageSize)))[index % pageSize] = ((int[]) bufferArrayList.get((index >> exp)))[index & r] = newValue; } |
9. Don’t ignore exceptions
public static Bundle decodeUrl(String s) { Bundle params = new Bundle(); if (s != null) { String array[] = s.split("&"); for (String parameter : array) { String v[] = parameter.split("="); try { params.putString(URLDecoder.decode(v[0], "UTF-8"), URLDecoder.decode(v[1], "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } } return params; } |
Printing stack traces should be almost always avoided in production code. This is as bad as ignoring the exceptions. This writes to standard error stream, which is not where the log goes to using a logging framework. (link to source code)
You may want to check out top 10 questions about Java exceptions on StackOverflow.
Can you explain how to use Exceptions with Loggers?