Lazy Thread-Safe Collections in Java

I recently needed to update some Java code that was frequently making unecessary calls to an external web service. Depending on the particular UI component, or the item being displayed in that component, a large set of the data being retrieved ended up not being displayed. My task was to update the code so that it only made external calls to the web service when the data was going to be displayed.

My first thought was to pass along a flag from the view layer that would indicate whether or not to retrieve the data in question. This would require updating all of the UI classes that displayed the data, as well as passing the new flag through several additional method calls to prevent the data from being collected when it wasn’t needed.

I did not care for this solution because it meant changing several classes so that details of the view could be passed down into the data retrieval logic. Instead, I came up with an alternative solution, the crux of which is a generic, thread-safe, delayed evaluation Map.

Instead of the business layer service returning a fully populated Map of data, it returns a Map that will contain that data, if it is accessed. If no user interface components request the data for display, the data will never be retrieved. This meant that I did not have to change any code outside of wrapping the logic for populating the Map with a new LazyImmutableMap. Here is an example of how it is used:

1
2
3
4
5
6
7
8
9
10
11
public Map<String, String> getData(final Integer id) {
  return new LazyImmutableMap<String, String>(
    
    new Callable<Map<String, String>() {

      public Map<String, String> call() throws Exception {      
        return externalService.getSomeData(id);
      }
    
  });
}

The call to externalService.getSomeData() will only be invoked if something tries to access an element in the LazyImmutableMap. Implementing the class itself turned out to be extremely simple with the help of Google Collections’ ForwardingMap (see this previous post for more on Google Collections) and the standard Java concurrency class FutureTask.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class LazyImmutableMap<K,V> extends ForwardingMap<K,V> {
  public static class AccessException extends RuntimeException {
    public AccessException(Throwable cause) {
      super(cause);
    }
  }

  private final FutureTask<Map<K, V>> task;

  public LazyImmutableMap(final Callable<Map<K,V>> eval) {    
    task = new FutureTask<Map<K,V>>(new Callable<Map<K,V>>() {
      public Map<K, V> call() throws Exception {
        return ImmutableMap.copyOf(eval.call());
      }
    });
  }

  @Override
  protected Map<K, V> delegate() {
    task.run();
    try {
      return task.get();
    } catch (InterruptedException e) {
      throw new AccessException(e);
    } catch (ExecutionException e) {
      throw new AccessException(e.getCause());
    }    
  }
}

The constructor creates a FutureTask that when accessed will return an immutable copy of the Map provided by the passed in Callable.

Extending ForwardingMap means that I only needed to implement the delegate method to get a fully compliant Map class. The FutureTask takes care of preventing the passed in Callable from being executed more than once, and it’s thread-safe, so any number of threads can access an instance of the map simultaneously.

Unfortunately Java’s checked exceptions add quite a bit of clutter to an otherwise very clean and simple class. The AccessException is needed because the normal Map interface does not allow for checked exceptions to be thrown. The get() method on a FutureTask can throw two different types of exceptions. They need to be handled differently, so they are both caught and properly wrapped in an AccessException (a subclass of RuntimeException) and re-thrown.

Using this technique I was able to retrieve the external data if and only if it was going to be displayed to the user. And I was able to do it without breaking the separation between the view and the business logic.