简体   繁体   中英

Java: read-only view of the union of two Maps?

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:

  • take all entries from the first map
  • take the entries from the second map whose key does not appear in the first one

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM