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