Is there an implementation of java.util.Map
in a common library (Guava, commons-collections, etc.) that serves as a read-only view of a union of two backing maps? Ie, it would behave like union
below, but without explicitly creating a new map instance, and reflecting future updates to the underlying maps:
Map<K, V> a, b; // From somewhere
Map<K, V> union = new HashMap<>();
union.putAll(b);
union.putAll(a);
My searching hasn't turned up anything.
When you implement it yourself, you can extends AbstractMap
whose only mandatory method to implement, is entrySet()
. Implementing the entry set by extending AbstractSet
requires an implementation for iterator()
and size()
. That's the minimum required to implement a map.
Since a key may appear in both maps, we can not simply sum up the map's sizes. So it has to be based on the same iteration logic as the iterator()
implementation:
Note that it's possible that the resulting size exceeds the int
value range. There's no real solution to that. Collection.size()
specifies that Integer.MAX_VALUE
will be returned when there are more elements, but I wouldn't be surprised when this policy turns out to be unknown to many developers.
When implementing an iterator or similar facility handing out Map.Entry
instances, we have to take care not to use the original map's entries, as that would allow modifying the original map through the entry's setValue
method.
The iteration logic can be expressed concisely using the Stream API:
public class MergedMap<K,V> extends AbstractMap<K,V> {
final Map<K,V> first, second;
public MergedMap(Map<K, V> first, Map<K, V> second) {
this.first = Objects.requireNonNull(first);
this.second = Objects.requireNonNull(second);
}
// mandatory methods
final Set<Map.Entry<K, V>> entrySet = new AbstractSet<Map.Entry<K, V>>() {
@Override
public Iterator<Map.Entry<K, V>> iterator() {
return stream().iterator();
}
@Override
public int size() {
long size = stream().count();
return size <= Integer.MAX_VALUE? (int)size: Integer.MAX_VALUE;
}
@Override
public Stream<Entry<K, V>> stream() {
return Stream.concat(first.entrySet().stream(), secondStream())
.map(e -> new AbstractMap.SimpleImmutableEntry<>(e.getKey(), e.getValue()));
}
@Override
public Stream<Entry<K, V>> parallelStream() {
return stream().parallel();
}
@Override
public Spliterator<Entry<K, V>> spliterator() {
return stream().spliterator();
}
};
Stream<Entry<K, V>> secondStream() {
return second.entrySet().stream().filter(e -> !first.containsKey(e.getKey()));
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
return entrySet;
}
// optimizations
@Override
public boolean containsKey(Object key) {
return first.containsKey(key) || second.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return first.containsValue(value) ||
secondStream().anyMatch(Predicate.isEqual(value));
}
@Override
public V get(Object key) {
V v = first.get(key);
return v != null || first.containsKey(key)? v: second.get(key);
}
@Override
public V getOrDefault(Object key, V defaultValue) {
V v = first.get(key);
return v != null || first.containsKey(key)? v:
second.getOrDefault(key, defaultValue);
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
first.forEach(action);
second.forEach((k,v) -> { if(!first.containsKey(k)) action.accept(k, v); });
}
}
Since it implements the iteration logic via the Stream API, the entry set also overrides the Stream related methods providing that Stream directly. This avoids inheriting implementations resorting to the iterator actually backed by a Stream.
Further, the map itself implements the common query methods for efficiency. The logic is similar, use the first map's entry if present, the second map's otherwise. Special attention is required to the possibility to have a key explicitly mapped to null
, as some maps allow. The methods above use the optimistic approach, when a non- null
value is present, only a single lookup is required.
I quite like Holger's answer, but here's a simpler approach that is easily understandable and debuggable. It delegates all read only methods to the union that is calculated on demand:
public class UnionMap<K,V> extends AbstractMap<K,V> {
private Map<K,V> a, b;
public UnionMap(Map<K, V> a, Map<K, V> b) {
this.a = Objects.requireNonNull(a);
this.b = Objects.requireNonNull(b);
}
private Map<K,V> union() {
Map<K, V> union = new HashMap<>(b);
union.putAll(a);
return union;
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
return union().entrySet();
};
@Override
public boolean containsKey(Object key) {
return union().containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return union().containsValue(value);
}
@Override
public V get(Object key) {
return union().get(key);
}
@Override
public V getOrDefault(Object key, V defaultValue) {
return union().getOrDefault(key, defaultValue);
}
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
union().forEach(action);
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.