Wednesday 29 August 2012

Java: adding state to interfaces in Java 8 (pre-release)

Java 8 introduces the concept of default methods to interfaces and this post looks at the cost of adding state to them.

This information pertains to the pre-release version of Java 8 mentioned in a previous post.

Abstract classes versus interfaces in Java 8

Feature Abstract Classes Java 8 Interfaces
Inheritance classes can extend single abstract class classes can implement multiple interfaces
Concrete methods any visibility public (with default keyword)
Abstract methods prohibits private public only
Member variables any visibility public static final only

Default methods

Imagine we want a general way to add listeners of this form to our types and the type hierarchy prohibits an abstract parent.

A simple listener interface:

public interface Listener<E> {
  void handle(E event);
}

Private variables are still prohibited in interfaces. The simplest way to add state would be to require the implementation to provide it:

import java.util.List;

public interface Listenable<E> {
  
  List<Listener<E>> getListenerStore();
  
  public void addListener(Listener<E> l) default {
    getListenerStore().add(l);
  }

  public void removeListener(Listener<E> l) default {
    getListenerStore().remove(l);
  }

  public void handle(E event) default {
    for(Listener<E> l : getListenerStore()) {
      l.handle(event);
    }
  }
}

It would be nice if the type didn't leak the list of listeners to API consumers.

Adding private state

A level of indirection is required to hide state. The results are ugly.

This type uses weak references to map instances to state:

import java.util.Map;
import java.util.WeakHashMap;

public final class WeakStore<K, V> {
  private Map<K, V> store = new WeakHashMap<>();

  public V value(K key, Factory<V> factory) {
    V value = store.get(key);
    if (value == null) {
      value = factory.create();
      store.put(key, value);
    }
    return value;
  }

  public static interface Factory<T> {
    T create();
  }
}

This is used in a static context by the interface via a utility class:

import java.util.ArrayList;
import java.util.List;

final class ListenableStore {
  private static final WeakStore<Listenable<?>, List<Listener<?>>> STORE = new WeakStore<>();
  private static final WeakStore.Factory<List<Listener<?>>> FACTORY = ArrayList::new;

  private ListenableStore() {}

  public static <E> void addListener(Listenable<E> listenable, Listener<E> l) {
    STORE.value(listenable, FACTORY)
        .add(l);
  }

  public static <E> void removeListener(Listenable<E> listenable, Listener<E> l) {
    STORE.value(listenable, FACTORY)
        .remove(l);
  }

  public static <E> void handle(Listenable<E> listenable, E event) {
    for (Listener<?> l : STORE.value(listenable, FACTORY)) {
      @SuppressWarnings("unchecked")
      Listener<E> listener = (Listener<E>) l;
      listener.handle(event);
    }
  }
}

The interface just calls its store to retrieve the state using itself as a key:

public interface Listenable<E> {
  public void addListener(Listener<E> l) default {
    ListenableStore.addListener(this, l);
  }

  public void removeListener(Listener<E> l) default {
    ListenableStore.removeListener(this, l);
  }

  public void handle(E event) default {
    ListenableStore.handle(this, event);
  }
}

Finally, a unit test:

import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertNull;

public class ListenableTest {
  @Test
  public void testListenable() {
    class ListeningThing implements Listenable<Object> {}
    
    Object event = new Object();
    final AtomicReference<Object> ref = new AtomicReference<>();
    Listener<Object> listener = ref::set;
    
    ListeningThing thing = new ListeningThing();
    thing.addListener(listener);
    thing.handle(event);
    
    assertTrue(event == ref.get());
    ref.set(null);
    
    thing.removeListener(listener);
    thing.handle(event);
    assertNull(ref.get());
  }
}

There is room for improvement, but this interface is probably stretching default methods beyond their intended purpose. I'm not sure I would use this in production-level code. Once you start considering interactions with other JVM features like threading and serialization it probably isn't worth the hassle.

No comments:

Post a Comment

All comments are moderated