Wednesday, 22 August 2012

Java: checked exceptions and lambdas in Java 8 (pre-release)

Update: Java 8 has been released and I've implemented a library with a more elegant approach to exception handling than the one described in this post: details + downloads; source code; blog posts.


This post looks at the effect of lambdas on checked exception handling.

This code uses a pre-release Java 8 build. See the previous post for more.

The problem with checked exceptions

The Java compiler forces developers to deal with checked exceptions. This can make using patterns like the execute-around idiom burdensome. Read Brian Goetz's Java theory and practice: The exceptions debate for a succinct introduction to the topic.

An attempt to tackle boiler-plate

This type queues requests to be executed in the calling thread:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ExecutionQueue {
  private Lock lock = new ReentrantLock();

  public void execute(Runnable runnable) {
    lock.lock();
    try {
      runnable.run();
    } finally {
      lock.unlock();
    }
  }
}

Callers need to jump through hoops to handle checked exceptions correctly. This code does so by using a try/catch to wrap them in an unchecked exception:

import java.io.File;
import java.io.IOException;
import com.google.common.io.Files;

public class IO {
  private static ExecutionQueue QUEUE = new ExecutionQueue();

  /** @throws RuntimeIOException */
  public static void writeToFile(final byte[] data, final File file) {
    QUEUE.execute(() -> {
      try {
        Files.write(data, file);
      } catch (IOException e) {
        throw new RuntimeIOException(e);
      }
    });
  }

  public static class RuntimeIOException extends RuntimeException {
    RuntimeIOException(Exception e) {
      super(e);
    }
  }
}

This code uses the Google Guava Files type to write data to a file. The lock stops two writes happening at the same time. The RuntimeIOException will need to be handled further down the call stack.

The try/catch can be eliminated using a utility type (Unchecked - see below), a static import (invoke), another lambda expression and a constructor reference:

  public static void writeToFile(final byte[] data, final File file) {
    QUEUE.execute(() -> {
        invoke(() -> {
          Files.write(data, file);
        }, RuntimeIOException::new);
    });
  }

Handling checked exceptions that never happen

The Java platform is required to support UTF-8, so this encoding method will never throw the declared exception:

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.Map;
import java.util.Map.Entry;

public class UrlQuery {
  public String encode(Map<String, String> params) {
    String encoding = "UTF-8";
    StringBuilder buf = new StringBuilder();
    for (Entry<String, String> entry : params.entrySet()) {
      try {
        String key = URLEncoder.encode(entry.getKey(), encoding);
        String value = URLEncoder.encode(entry.getValue(), encoding);
        String delim = buf.length() == 0 ? "" : "&";
        buf.append(delim).append(key).append("=").append(value);
      } catch (UnsupportedEncodingException e) {
        throw new AssertionError(e);
      }

    }
    return buf.toString();
  }
}

Here is the alternative method implementation:

  public String encode(Map<String, String> params) {
    final String encoding = "UTF-8";
    final StringBuilder buf = new StringBuilder();
    for (final Entry<String, String> entry : params.entrySet()) {
      invoke(() -> {
        String key = URLEncoder.encode(entry.getKey(), encoding);
        String value = URLEncoder.encode(entry.getValue(), encoding);
        String delim = buf.length() == 0 ? "" : "&";
        buf.append(delim).append(key).append("=").append(value);
      });
    }
    return buf.toString();
  }

Conclusions

Lambdas are not an ideal way to tackle checked exception noise in Java classes. There is a potential performance penalty in creating the object instances the lambdas implement. Whether you prefer the lambda syntax over the try/catch approach is a stylistic choice.

Eliminating unnecessary try/catch blocks in Java is still likely to require its own language feature.

The Unchecked type implementation

public final class Unchecked {
  /**
   * Traps exceptions thrown by the {@link Functor} and wraps them in the
   * type provided by the {@link ExceptionFactory}.
   */
  public static void invoke(Functor fn, ExceptionFactory factory) {
    try {
      fn.invoke();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw factory.wrap(e);
    }
  }

  public static void invoke(Functor fn) {
    invoke(fn, ERROR_FACTORY);
  }

  private static final ExceptionFactory ERROR_FACTORY = (e) -> {
      throw new AssertionError(e);
  };

  public static interface ExceptionFactory {
    RuntimeException wrap(Exception e);
  }

  public static interface Functor {
    void invoke() throws Exception;
  }
}

The Functor type was introduced because java.lang.Runnable.run() does not allow throwing checked exceptions. If a return type was required, methods consuming the java.util.Concurrent type (or an equivalent) could be used.

2 comments:

  1. "Lambdas are not an ideal way to tackle checked exception noise in Java classes. There is a potential performance penalty in creating the object instances the lambdas implement."

    You seem to assume that lambdas are implemented as anonymous classes. This is not the case (any more?). They are mostly implemented as methods and called by invokedynamic via method handles.

    See this video presentation where B.Goetz explains those details:
    http://www.youtube.com/watch?v=C_QbkGU_lqY

    ReplyDelete
    Replies
    1. Thanks very much for the link to the talk! I accept your point that lambdas will often be cheap and not involve artificially constructed type instances.

      I still think there will often be a performance penalty for the Unchecked type over the try/catch approach.

      1. Unchecked.invoke takes a Functor instance.

      2. In the UrlQuery.encode example we need to non-statically reference at least the parameter map and the character buffer.

      3. If there is an instance of Functor it will act like other object instances (with respect to garbage collection, etc.)

      Brian Goetz talks on the Performance costs slide about the worst case being equivalent to inner classes. He calls it the Capture cost for referencing things from the lexical context.

      I suspect more invocations that involve checked exceptions will involve capturing data than not.

      Note: my implementation of UrlQuery.encode is poor - it would be more efficient to put the loop inside the lambda as it would generate fewer Functor instances.

      Delete

All comments are moderated