Tip: When you can't throw an exception

Working with exception-free superclasses

Elliotte Rusty Harold considers the problem of implementing an interface, or subclassing a class, when a method you must override doesn't provide a sufficiently expansive throws clause. This tip explores your options when you can neither handle nor throw a checked exception.

Elliotte Rusty Harold, Software Engineer, Cafe au Lait

Photo of Elliotte Rusty HaroldElliotte Rusty Harold first learned the Java language in 1995. Prior to that, his first language was Fortran and his second was Applesoft Basic. C was probably his third language, and fourth may have been Microphone II. Fifth was Pascal, though he never did much with that. Sixth was probably IDL (Interactive Data Language). Seventh was perhaps Perl? Java was probably his eighth language, and the one he's taken farther than any other. However, since then he's continued to learn new languages, including PHP, AppleScript (or did that come before Java?), XSLT, XQuery, C++, and most recently Haskell.



06 April 2010

Also available in Chinese Japanese

One problem with checked exceptions is that sometimes you simply aren't allowed to throw them. In particular, if you are overriding a method declared in a superclass or implementing a method declared in an interface, and that method does not declare any checked exceptions, your implementation can't declare one either. This forces you to handle the exception prematurely. You can convert the exception to a run-time exception, or you can suppress it without handling it. But is this what you should be doing, or is something more deeply wrong here?

The problem

Looking at an example will make this problem clear. Suppose you have a List of File objects, and you want to sort them lexicographically by their canonical path, that is, by their full absolute path after resolving aliases, symbolic links, and /../ and /./. The naive approach uses a comparator, as shown in Listing 1:

Listing 1. Comparing two files by canonical path
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;

public class FileComparator implements Comparator<File> {

    public int compare(File f1, File f2) {
        return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
    }

    public static void main(String[] args) {
      ArrayList<File> files = new ArrayList<File>();   
      for (String arg : args) {
          files.add(new File(arg));
      }
      Collections.sort(files, new FileComparator());
      for (File f : files) {
          System.out.println(f);
      }
    }
    
}

Unfortunately, this code doesn't compile. The problem is that the getCanonicalPath() method throws an IOException because it needs to access the file system. Typically, when working with checked exceptions, you can take account of this in one of two ways:

  1. Wrap a try block around the offending code, and catch any exceptions that are thrown.
  2. Declare that the enclosing method, compare() in this example, also throws IOException.

Normally, the choice depends on whether you can plausibly handle the exception at the point where it's thrown. If you can, use a try-catch block. If you can't, declare that the enclosing method itself throws the exception. Unfortunately, neither of those techniques works with this example.

You can't plausibly handle an IOException inside the compare() method. Technically, you could — by simply returning 0 or 1 or -1, as shown in Listing 2:

Listing 2. Returning a default value on exception
public int compare(File f1, File f2) {
    try {
        return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
    }
    catch (IOException ex) {
       return -1;
    }
}

However, this violates the contract of the compare() method because it's not a stable result. Call it twice with the same objects and you may get different answers. If the comparator is used for sorting, that can mean the list isn't sorted correctly in the end. So now try option 2 — declaring that compare() throws IOException:

public int compare(File f1, File f2) throws IOException {
    return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
}

This doesn't even compile. Because checked exceptions are part of a method's signature, you can't add one to an overriding variant any more than you can change its return type. That leaves option 1.5: catch the exception inside compare() and convert it into a run-time exception, which you can throw as shown in Listing 3:

Listing 3. Converting a checked exception to a run-time exception
public int compare(File f1, File f2) {
    try {
        return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
    }
    catch (IOException ex) {
       throw new RuntimeException(ex);
    }
}

Unfortunately, although it compiles, this approach doesn't work either, for reasons that are more subtle. The Comparator interface defines a contract (see Resources). That contract does not allow this method to throw a run-time exception (barring violations of generic type safety that qualify as bugs in the calling code). Methods that use this comparator legitimately depend on it to compare two files, without throwing any exceptions. They will not be prepared to handle an exception that unexpectedly bubbles up from compare().

Indeed this subtlety is precisely why run-time exceptions are a bad idea for external conditions that should be handled by code. They allow you to sweep the problem under the rug without really dealing with it. All the bad effects of not handling the exceptions remain, including corrupted data and incorrect results.

You're caught on the horns of a dilemma. You can't realistically handle the exception inside compare(), and you can't handle the exception outside compare(). What's left — System.exit()? The only correct solution is to avoid the dilemma completely. Fortunately you have at least two ways to do this.


Split the problem

The first solution is to split the problem into two pieces. The comparison doesn't cause the exception. That is just strings. The exception is caused by converting files into strings via canonical paths. If you separate the operations that can throw an exception from those that can't, the problem becomes more tractable. That is, first convert all the file objects into strings, then sort the strings through a string comparator (or even the natural ordering of java.lang.String), and finally use the sorted list of strings to sort the original list of files. This approach is a little less direct, but it has the advantage of throwing the IOException before the list is mutated. If an exception occurs, it occurs at a well-defined point before any damage is done, and the invoking code can figure out how to deal with it. Listing 4 demonstrates:

Listing 4. First read, then sort
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

public class FileComparator {

    private static ArrayList<String> getCanonicalPaths(ArrayList<File> files) 
            throws IOException {
        ArrayList<String> paths = new ArrayList<String>();  
        for (File file : files) paths.add(file.getCanonicalPath());
        return paths;
    }
    
    public static void main(String[] args) throws IOException {
      ArrayList<File> files = new ArrayList<File>();   
      for (String arg : args) {
          files.add(new File(arg));         
      }
      
      ArrayList<String> paths = getCanonicalPaths(files);
      
      // to maintain the original mapping
      HashMap<String, File> map = new HashMap<String, File>();
      int i = 0;
      for (String path : paths) {
          map.put(path, files.get(i));
          i++;
      }
      
      Collections.sort(paths);
      files.clear();
      for (String path : paths) {
          files.add(map.get(path));
      }
    }
    
}

Listing 4 does not remove the possibility of an I/O error. You can't do that because it's a function of forces outside your code. But you have moved that issue to a place where it's more workable.


Avoid the problem

The approach mentioned above is a little complex, so I'll suggest a second solution: Don't use the built-in compare() function or Collections.sort() at all. Consider that even though it can be convenient, it may not appropriate for this use case. Comparable and Comparator were designed for situations where the comparison operation is deterministic and predictable. Once I/O enters the picture that ceases to be the case. Chances are that the usual algorithms and interfaces do not apply. Even if they do work, they may be horribly inefficient.

For instance, suppose that instead of comparing files by their canonical paths, you're comparing them by their contents. Each comparison operation needs to read the contents — maybe even the complete contents — of the two files it's comparing. If so, an efficient algorithm would want to minimize the number of reads, and it might well want to cache the results of each read — or perhaps a hashcode of each file if they're large — rather than re-read each file each time it's compared. Again, you're driven to the idea of first filling a list of comparison keys and then sorting, rather than sorting inline.

You could imagine defining a separate, parallel IOComparator interface that does throw the necessary exceptions, as shown in Listing 5:

Listing 5. An independent IOComparator interface
import java.io.IOException;

public interface IOComparator<T> {

    int compare(T o1, T o2) throws IOException;
    
}

Then you define a separate, parallel tree of utilities based on this class that takes the necessary care to work on temporary copies of collections, so that it can throw exceptions without leaving data structures in potentially corrupted, intermediate states. For example, Listing 6 provides a basic bubble sort:

Listing 6. Bubble sorting files
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class IOSorter {

    public static <T> void sort(List<T> list, IOComparator<? super T> comparator) 
      throws IOException {
        List<T> temp = new ArrayList<T>(list.size());
        temp.addAll(list);
        
        bubblesort(temp, comparator);
        
        // copy back to original list now that no exceptions have been thrown
        list.clear();
        list.addAll(temp);
    }
    
    // of course you can replace this with a better algorithm such as quicksort
    private static <T> void bubblesort(List<T> list, IOComparator<? super T> comparator) 
      throws IOException {
        for (int i = 1; i < list.size(); i++) {
            for (int j = 0; j < list.size() - i; j++) {
                if (comparator.compare(list.get(j), list.get(j + 1)) > 0) {
                    swap(list, j);
                }
            }
        }
    }

    private static <T> void swap(List<T> list, int j) {
        T temp = list.get(j);
        list.set(j, list.get(j+1));
        list.set(j + 1, temp);
    }
 
}

This is hardly the only approach. For clarity, Listing 6 deliberately parallels the existing Collections.sort() method; but it may well make more sense to return a new list rather than mutate the old list, precisely to avoid the issue of what happens when an exception is thrown in the middle of a mutation.

Finally, now that you're actually acknowledging and dealing with the real possibility of an I/O error, rather than sweeping it under the rug, you can do even more sophisticated error correction. For instance, an IOComparator might not take an I/O error lying down, but — because many I/O problems are transient — you can retry a few times, as shown in Listing 7:

Listing 7. If at first you don't succeed, try, try again (but not too many times)
import java.io.File;
import java.io.IOException;

public class CanonicalPathComparator implements IOComparator<File> {

    @Override
    public int compare(File f1, File f2) throws IOException {
        for (int i = 0; i < 3; i++) {
            try {
                return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());
            }
            catch (IOException ex) {
                continue;
            }
        }
        // last chance
        return f1.getCanonicalPath().compareTo(f2.getCanonicalPath());  
    }

}

This technique wouldn't solve the problem for a regular Comparator because you'd have to retry indefinitely to avoid throwing the exception, and many I/O problems aren't transient.


Are checked exceptions a bad idea?

Would all this have been any different if java.io.IOException were a run-time exception instead of a checked exception? The answer is yes. If IOException extended RuntimeException instead of java.lang.Exception, it would be much easier to write buggy, incorrect code that ignores the real possibility of an I/O error and fails unexpectedly at run time.

However, it would not be easier to write correct code that prepares for and handles the I/O error. Yes, this approach is more complicated than one where unexpected I/O errors never occur and where you needn't plan for them. However, eliminating checked exceptions from the Java language would do nothing to bring us to that happy state. I/O errors and other environmental problems are a fact of life, and it's far better to be prepared for them than to ignore them.

The bottom line is that checked exceptions are part of a method's signature for good reason. When you find yourself trying to throw a checked exception from a method that won't allow it to be thrown — and thereby suppressing an exception you shouldn't suppress — back up, regroup, and consider why you're overriding that method in the first place. Chances are good you should be doing something else entirely.

Resources

Learn

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=480203
ArticleTitle=Tip: When you can't throw an exception
publish-date=04062010