P8tech logo  

Guidelines for Using Exceptions in Java

You may want to read and adopt the guidelines that help you get the most out of Java’s exception handling mechanism. You can write powerful and effective Java code by adopting most, or all, of the things recommended here.

Remember that exception handling provides a way to process synchronous errors such as divisions by zero and out-of-range array indexes. It’s not meant for handling asynchronous events such as disk I/O completions and mouse clicks and keystrokes.

When to use Exceptions

You may be wondering as to how you actually decide to create and throw an exception. As a rule of thumb, create exception classes for all common exceptions that are likely to occur in multiple classes of an application. In order to deal with simple errors that are likely to occur only in individual methods, try not to create an exception class. You can use an if statement in most cases to trap these types of errors.

Here’s a simple example that shows when not to use a try-catch block in your code. The code throws an error when there’s an exception of the type NullPointerException.

 

try {

   System.out.println(refvar.toString()};

}

catch (NullPointerException e)  {

   System.out.println(“refVar is null!”);

 

This is pure overkill. You can instead use the much simpler if statement to handle these types of simple errors.

  

if (refVar !=null)

   System.out.println(refVar.toString()};

else

   System.out.println(“refVar is null”);

 

If your code can perform a simple logic test, as shown here, to handle an error, do so, rather than using an exception object to handle the error.

 

When you neither catch nor throw exceptions

When you call any method that throws a checked exception, you must either throw the exception or catch it. If you decide that you can’t handle the exception in the method where it occurs, write a throw statement to send the exception up to the calling method, which must then either handle it with an exception handler, or throw it up to its calling method.

The following set of examples show how you must either catch or throw a checked exception, in order to avoid compiler errors.

The first example shows code that throws an IOException. The statement in the method getFileLength may throw an IO exception when it calls the constructor of the RandomAccessFile class. In addition, the statement that calls the length method of the RandomAccessFile object may also throw an IOException. You thus specify the throws clause in the declaration for the getFileLength method to trap IOException.

 

public static long getFileLength() throws IOException

{

      RandomAccessFile in = new RandomAccessFile(“mytext.dat”, “r”);

      long length = in.length();

      return length;

}

 

Our next example shows code for a method that calls the getFileLength method. Since we already know that the getFileLength method call throws an IOException error, our new method must either handle the IO Exception or throw it. Our example here shows how to handle the IOException by catching it with a try statement:

 

public static int getRecordCount()

{  

        try

        {

               long length = getFileLength();   // this  may throw an IOException

               int recordCount = (int) (length / /RECORD_SIZE);

               return recordCount;

         }

          catch (IOException e)                // this will catch the IOException

           {

                  System.out.println(“IO Exception!”);

                  return 0;

            }

}

 

The third example shows how a method can call the getFileLength method without catching IOException. Instead, it throws the exception.

 

public static int getRecordCount() throws IOException

{

         long length = getFileLength();        //this may throw an IOException

         int recordCount = (int) (length / RECORD_SIZE);

         return recordCount;

}

 

The getRecordCount method here includes a throw clause, which ensures that any IOException is thrown up to the calling method.

Our fourth example shows what happens when you don’t catch an exception or throw it.

 

public static int getRecordCount()

{

         long length = getFileLength();        //this may throw an IOException

         int recordCount = (int) (length / RECORD_SIZE);

         return recordCount;

}

 

Since your code fails to catch or throw a checked exception, you’ll receive a compile error:

 

MyExceptionTest.java.26: unreported exception java.io.IOException;

must be caught or declared to be thrown

 

The following is a quick summary of best practices and guidelines that enable you to take advantage of Java’s exception handling mechanism, without succumbing to common missteps in the use of exceptions.

 

Begin Early

It is a good idea to incorporate exception handling into your applications from the get go, before you even start programming. That is, you must do this at the design stage, as it gets much harder to do it after implementing your applications.

 

Don’t Ever Ignore an Exception

Regardless of whether you’re dealing with a checked or an unchecked exception, don’t ever ignore the exception. Sometimes, you might see code such as the following:

 

//Following is an empty catch block

try {

    

} catch (SomeException e)  {

}

 

The empty catch block means that the exception is simply ignored – it’s not dealt with. You don’t want to do this in your programs. At the minimum, if you must really, really ignore the exception, put a comment explaining why you wanted to ignore the exception. You can use printStackTrace to output a simple error message to tell you there was a problem. If you ignore the exception, because of a condition that you’ve predicted, it means that the program will run on despite the error. However, you’re going to run the risk of a complete program failure down the road when conditions change. If you propagate the exception out, you can ensure that the program fails quickly and captures information that helps you fix the error.

 

Don’t use Exception Handling for every Statement

The goal of exception handling is to clarify programs – try not to use a try/catch/finally mechanism for every potential exception generating statement. A simple strategy is to enclose a significant amount of code within the try block and specify multiple catch blocks to account for all possible exceptions. End the whole thing with a single finally block if you need to release any resources held by the program. In any case, avoid placing try/catch/finally around each statement that might throw an exception. Your goal is to use exception handling to remove error processing code from the main program code.

 

Maintain Failure Atomicity

Always strive to ensure that when an object throws an exception, the object continues to remain in a well-defined and usable state. Ideally, even after a method fails due to an exception, the object should be left in the state it was before the method was invoked, When a method can do this, it’s said to possess the property of failure atomicity.

You can achieve failure atomicity by using any of the following programming techniques.

 

Design Immutable Objects

The easiest way to achieve failure atomicity is by designing immutable objects. Although when you create an object you can change its contents, occasionally, it’s a good idea to create objects whose contents cannot be changed after creation. An object such as this is called an immutable object and its class, an immutable class. A good example is the String class, which is an immutable class. You get automatic failure atomicity when an object is immutable, because the state of an object can’t be modified after you create the object.

 

Check Parameters for Validity before the Operation

If you’re stuck with mutable objects, the best way to achieve failure atomicity is to perform a check for the validity of parameters before the program commences the modification of an object. The following example shows how you do this:

 

public Object pop() {

    if (size == 0)

        throw new EmptyStackException();

    Object result = elements[--size];

    elements[size] = null; // Eliminate obsolete reference

    return result;

}

 

The initial size check ensures that the method will do two things when it tries to pop an element from an empty stack: it will throw an exception and it will do so while leaving the size field in a consistent state. Otherwise, the attempt to pop the element from an empty stack will leave the size field in a negative state which, of course, is an inconsistent state for the object to be in.

 

Attempt to Execute the Code before you Modify the Object

You can write your programs in a way where the code that is susceptible to a potential exception, is run before any code that modifies an object. A good example would be where you would like to add an element to a TreeMap, wherein the new element must be compared using the TreeMap’s ordering. If your code attempts to add an incorrect type element, it will fail when a search for the element in the tree is made, before the tree is modified.

 

Intercept the Failure

Another strategy to achieve failure atomicity is to put in code that intercepts a failure that occurs during an operation, and which rolls back the object’s state to what it was before the operation began. This strategy is often adopted when modifying disk based data, that is, data contained in a relational database, for example.

 

Perform Operations on a Temporary Copy of the Object

You can limit state changes made to an object by changing a temporary copy of the object. You replace the contents of the object with the temporary copy once you complete the computational operations. If, for some reason, the computational work (say, a sort) ends up failing, no harm is done to the original object, as you’re working with just a copy of the object.

 

Collect Failure Details

If you’re troubleshooting an easily reproducible failure, it’s no big deal whether you’re collecting detailed failure information. When dealing with exceptions that are not easily reproducible, it is critical that your exception’s toString method captures sufficient details about the failure - to help diagnose and fix the failure.

When an exception occurs, your details message of an exception should capture the current values of all parameters and values that contributed to the exception. If your error message simply states IndexOutofBoundException, it’s only of limited use and may, or may not, help you fix the data problem.  If, on the other hand, your detail message for the IndexOutofBoundException contains the lower and upper bounds and the index value, you have something meaningful in your hands with which to proceed towards a fix of the failure condition.

Since the stack trace of the exception always contains the precise file and line number that threw the exception, you don’t need a lot of information about the source code in your exception messages. To ensure that your exceptions always contain adequate failure details in their details message, use one of the constructors I explained about earlier in this chapter. It is better to do this than to capture the details within a simple string detail message.

The following example shows how you can use a more helpful constructor than the simple String constructor.

 

/**

 * Construct an IndexOutOfBoundsException.

 *

 * @param lowerBound the lowest legal index value.

 * @param upperBound the highest legal index value plus one.

 * @param index the actual index value.

 */

public IndexOutOfBoundsException(int lowerBound, int upperBound,

                                 int index) {

    // Generate a detail message that captures the failure

    super("Lower bound: "    + lowerBound +

          ", Upper bound: "  + upperBound +

          ", Index: "        + index);

    // Save failure information for programmatic access

    this.lowerBound = lowerBound;

    this.upperBound = upperBound;

    this.index = index;

}

 

Star Provide accessor methods for all exceptions, especially for checked exceptions.

 

As mentioned earlier, it’s sometimes useful to provide accessor methods to capture details regarding exactly why a failure happened. In the case of the example just shown, you can, for example, provide accessor methods for lowerBound, upperBound and index. It’s critical that you provide such accessor methods for all accessor methods.

 

Use Standard Java Exceptions

As discussed earlier in the book, the Java platform offers a built-in set of unchecked exceptions which will take care of most of your exception mechanism setting needs. Make a point to try and use these preexisting exceptions before you run off coding your own custom exceptions.  Other programmers are familiar with these preexisting exceptions and, therefore, they can easily understand your code.  Also, since you’ll be using just a set of basic exception classes and not a whole bunch of custom exceptions, your programs need less time to load.

Here are brief descriptions of the most common standard Java exceptions.

 

IllegalArgumentException


You throw this exception when a caller passes in an argument with an inappropriate value. For example, a calling program that passes a negative number in a parameter that denotes how many times you repeat an action.

IllegalStateException


This is the exception thrown when an object is illegally invoked because of its state – say, when a calling program tries to use an object before it has been initialized.

NullPointerException


Used when a parameter value is null when it ought not to be.

IndexOutofBoundsException


An exception thrown when a caller passes an out of range value in a parameter representing an index into a sequence.

ConcurrentModificationException


Thrown when an object designed for single thread usage finds that it was, or is, being concurrently modified.

UnsupportedOperationException


Thrown when an object doesn’t support a specific operation such as when a program tries to delete an element from an append-only list.

Although in most cases it’s quite apparent as to which exception applies to an error condition, sometimes, you may find that more than one exception might be correctly used for an error condition. There aren’t any cut and dry rules for the specification of exceptions – as with some other things in Java, handling exceptions is an art as well as a science!
 

Document All of a Method’s Exceptions

Spend the necessary time to fully document all exceptions thrown by each method in an application. Declare all checked exceptions individually and use the Javadoc tag @throws to document the conditions under which each of the exceptions can be thrown.

It is critical that you don’t fall into the bad habit of declaring that a method throws some super class of multiple exception cases that the method is set up to throw. That means that you never declare a method that can throw the generic exception classes Exception or Throwable! Using generic exceptions might handle the exception all right, but it keeps you from understanding the precise error that caused the exception to be thrown.

tip Always attempt to document every single exception that can potentially be thrown by each method in a program.

Take care to document both checked and unchecked exceptions. Since unchecked exceptions normally represent programming errors, documenting them can keep you from avoiding those errors. You can use the same Javadoc tag @Throws to document unchecked exceptions, as is the case for checked exceptions.

If multiple methods in a class can throw the same exception, you may document the exception once in the class’s documentation comment. This is much more efficient than documenting the exception separately for each of the methods.

Finally, a useful rule of thumb when handling Java exceptions is to always throw early and catch late!

This finally wraps up our discussion of Java’s exception handling mechanisms and how to make the best use of them in your code.  Our next topic is the effective use of pre and post conditions in your code within the framework of what’s called Java assertions, primarily as a way of testing code before you move it to production.

Java Masterclass: Java Exceptions,
Assertions and Logging

Buy Now UK     Buy Now USA