Contents


Tip: When you can't throw an exception

Working with exception-free superclasses

Comments

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 Related topics). 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.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

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