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