Is there a Map implementation with listeners for Java?
I'm not aware of any standard or 3rd party, but it is easy, just create a class which wraps another Map and implements the Map interface:
public class MapListener<K, V> implements Map<K, V> {
private final Map<K, V> delegatee;
public MapListener(Map<K, V> delegatee) {
this.delegatee = delegatee;
}
// implement all Map methods, with callbacks you need.
}
Here is a working example of a map which fires property change events on put and remove. The implementation is divided in two classes:
ListenerModel
Contains the methods related to adding and removing the change listeners and also a method for firing the property changes.
ListenerMap
Extends ListenerModel and implementes the java.util.Map interface by delegation. It fires the property changes in the put and remove method only. It would make sense to fire the properties on other methods like e.g. clear(), putAll().
ListenerModel
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class ListenerModel {
private final PropertyChangeSupport changeSupport = new PropertyChangeSupport(this);
public void addPropertyChangeListener(PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
protected void firePropertyChange(String propertyName, Object oldValue, Object newValue) {
changeSupport.firePropertyChange(propertyName, oldValue, newValue);
}
}
ListenerMap
import java.util.*;
public class ListenerMap<K, V> extends ListenerModel implements Map<K, V> {
public static final String PROP_PUT = "put";
public static final String REMOVE_PUT = "remove";
private Map<K, V> delegate = new LinkedHashMap<>();
@Override
public void clear() {
delegate.clear();
}
@Override
public boolean containsKey(Object key) {
return delegate.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return delegate.containsValue(value);
}
@Override
public Set<Entry<K, V>> entrySet() {
return delegate.entrySet();
}
@Override
public V get(Object key) {
return delegate.get(key);
}
@Override
public boolean isEmpty() {
return delegate.isEmpty();
}
@Override
public Set<K> keySet() {
return delegate.keySet();
}
@Override
public V put(K key, V value) {
V oldValue = delegate.put(key, value);
firePropertyChange(PROP_PUT, oldValue == null ? null : new AbstractMap.SimpleEntry<>(key, oldValue),
new AbstractMap.SimpleEntry<>(key, value));
return oldValue;
}
@Override
public void putAll(Map<? extends K, ? extends V> m) {
delegate.putAll(m);
}
@Override
public V remove(Object key) {
V oldValue = delegate.remove(key);
firePropertyChange(REMOVE_PUT, oldValue == null ? null : new AbstractMap.SimpleEntry<>(key, oldValue),
null);
return oldValue;
}
@Override
public int size() {
return delegate.size();
}
@Override
public Collection<V> values() {
return delegate.values();
}
}
Here is a JUnit 4 test:
import org.junit.Before;
import org.junit.Test;
import java.beans.PropertyChangeListener;
import java.util.Map;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.Assert.assertThat;
/**
* Created by Gil on 01/07/2017.
*/
public class ListenerMapTest {
private ListenerMap<String, String> map;
@Before
public void setUp() throws Exception {
map = new ListenerMap<>();
}
@Test
public void whenPut_ShouldFireTrigger() throws Exception {
boolean[] fired = {false};
Map.Entry<String, String>[] listenEntry = new Map.Entry[1];
boolean[] checkNull = {true};
PropertyChangeListener propertyChangeListener = evt -> {
if (ListenerMap.PROP_PUT.equals(evt.getPropertyName())) {
if(checkNull[0]) {
assertThat(evt.getOldValue(), is(nullValue()));
}
else {
Map.Entry<String, String> oldValue = (Map.Entry<String, String>) evt.getOldValue();
assertThat(oldValue.getKey(), is("k1"));
assertThat(oldValue.getValue(), is("v1"));
}
listenEntry[0] = (Map.Entry<String, String>) evt.getNewValue();
fired[0] = true;
}
};
map.addPropertyChangeListener(propertyChangeListener);
map.put("k1", "v1");
assertThat(fired[0], is(true));
assertThat(listenEntry[0].getKey(), is("k1"));
assertThat(listenEntry[0].getValue(), is("v1"));
checkNull[0] = false;
map.put("k1", "v2");
}
@Test
public void whenRemove_ShouldNotFire() throws Exception {
boolean[] fired = {false};
PropertyChangeListener propertyChangeListener = evt -> {
fired[0] = true;
};
map.addPropertyChangeListener(propertyChangeListener);
map.put("k1", "v1");
assertThat(fired[0], is(true));
fired[0] = false;
map.removePropertyChangeListener(propertyChangeListener);
map.put("k2", "v2");
assertThat(fired[0], is(false));
}
}
Season to taste. This is representative, not normative. Of course it has issues.
public class ListenerMap extends HashMap {
public static final String PROP_PUT = "put";
private PropertyChangeSupport propertySupport;
public ListenerMap() {
super();
propertySupport = new PropertyChangeSupport(this);
}
public String getSampleProperty() {
return sampleProperty;
}
@Override
public Object put(Object k, Object v) {
Object old = super.put(k, v);
propertySupport.firePropertyChange(PROP_PUT, old, v);
return old;
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
propertySupport.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
propertySupport.removePropertyChangeListener(listener);
}
}