May 17, 2013

Checked or unchecked exception in Java - decide on the method responsible for performing the check approach

While reading Martin Fowler's book "Refactoring" I came across below paragraph suggesting when choose a checked and when an unchecked exception.

class Account... 
  int withdraw(int amount) {
      if (amount > _balance) return -1;
      else { _balance -= amount; return 0; 
  } 
  private int _balance;
}
To change this code to use an exception I first need to decide whether to use a checked or unchecked exception. The decision hinges on whether it is the responsibility of the caller to test the balance before withdrawing or whether it is the responsibility of the withdraw routine to make the check. If testing the balance is the caller's responsibility, it is a programming error to call withdraw with an amount greater than the balance. Because it is a programming error—that is, a bug—I should use an unchecked exception. If testing the balance is the withdraw routine's responsibility, I must declare the exception in the interface. That way I signal the caller to expect the exception and to take appropriate measures.

I think this is a great guideline, helping in this very, very, very frequent dilemma in the Java world.

I have entitled this approach decide on the method responsible for performing the check approach, it can be summarized as:

  • If it is the caller method should perform a check prior calling the called method (e.g. making sure the arguments are not not null) then if such check has not been done it is clearly a programming error and then the called method should thrown an unchecked exception.
  • It is the called method responsibility to make the check, because only this method can know how to perform such check then the exception thrown from the called method should be checked.

I have done some research checking how the above approach to the checked vs unchecked exception dilemma is applied in the Java world.

  1. java.io.File throws an unchecked NullPointerException when null is passed as the filename to the constructor
  2. public File(String pathname) {
      if (pathname == null) {
        throw new NullPointerException();
      }
      this.path = fs.normalize(pathname);
      this.prefixLength = fs.prefixLength(this.path);
    }
    

    It is the responsibility of the caller to make sure the mandatory arguments provided to the constructor are not nulls.

  3. java.io.File throws java.lang.IllegalArgumentException when an incorrect URL is passed to the constructor
  4. public File(URI uri) {
      if (!uri.isAbsolute())
        throw new IllegalArgumentException("URI is not absolute");
      if (uri.isOpaque())
        throw new IllegalArgumentException("URI is not hierarchical");
      String scheme = uri.getScheme();
      if ((scheme == null) || !scheme.equalsIgnoreCase("file"))
        throw new IllegalArgumentException("URI scheme is not \"file\"");
      if (uri.getAuthority() != null)
        throw new IllegalArgumentException("URI has an authority component");
      if (uri.getFragment() != null)
        throw new IllegalArgumentException("URI has a fragment component");
      if (uri.getQuery() != null)
        throw new IllegalArgumentException("URI has a query component");
      String p = uri.getPath();
      if (p.equals(""))
        throw new IllegalArgumentException("URI path component is empty");
      /// ...
    }
    

    Same story here, it is the caller who is responsible for making sure the provided arguments are correct.

  5. java.util.zip.InflaterInputStream throws a checked java.util.zip.DataFormatException when read() encounters a problem reading the ZIP file
  6. public int read(byte[] b, int off, int len) throws IOException {
      try {
        // ...
      } catch (DataFormatException e) {
        String s = e.getMessage();
        throw new ZipException(s != null ? s : "Invalid ZLIB data format");
      }
    }
    

    It is the responsibility of this (called) method to not only read a zip file but also to validate if the zip file is correct. If it is not valid, what is possible, the calling method should be notified about that.

  7. A checked java.rmi.AlreadyBoundException is thrown when a name provided to java.rmi.Naming.bind() is already bound
  8. public static void bind(String name, Remote obj) throws AlreadyBoundException // ...
    

    The responsibility of checking if the given name is already bound or not might be pushed onto the calling method as there is a method to check if the provided name is bound or not (Naming.lookup()). However it is possible that the name that has been checked to be available has been bound by another process/thread between the call to the lookup and bind method. Therefore a situation when this exception is thrown seems to be perfectly natural and should not be treated as a development bug on the caller side.

  9. A programming error of calling operations on a closed stream results does not result in an unchecked exception but in a checked java.io.IOException
  10. I is it important to keep in mind that this approach is only a guideline, which means there are cases when it is not followed, even in the JDK source code.

    Below code snippet from StringReader:
    public int read() throws IOException {
      synchronized (lock) {
        ensureOpen();
        if (next >= length)
          return -1;
        return str.charAt(next++);
      }
    }
    
    private void ensureOpen() throws IOException {
      if (str == null)
        throw new IOException("Stream closed");
    }
    

    Causes below code to throw a checked IOException:

    StringReader reader = new StringReader("aa");
    
    reader.read();  
    reader.close();  
    reader.read();
    

    While it is clear that is the programmer's responsiblity to make sure that he is not calling any operations on an already closed stream.

    Exactly the same exception is also thrown in the following IO classes:
    • BufferedInputStream
    • BufferedReader
    • BufferedWriter
    • CharArrayReader
    • PrintStream
    • PrintWriter

No comments:

Post a Comment