4
\$\begingroup\$

After finding myself sometimes needing a map of maps, and as I couldn't find one in existing libs (like Guava), I wrote my own implementation. Do you think this version is okay? Specifically: Is it acceptable that I don't permit empty "inner" maps as content for my "outer" map, hence probably breaking the contract of Map? Do I miss useful methods, are some methods superfluous? Should my class implement the Map interface at all, as some methods work counter-intuitive when using this abstraction? Are the names of the additional methods too subtle?

import java.util.*;

public class MapMap<K1,K2,V> implements Map<K1,Map<K2,V>> {

    private Map<K1, Map<K2,V>> underlyingMap = new HashMap<K1, Map<K2, V>>();

    public MapMap() {
    }

    public MapMap(Map<K1, Map<K2,V>> underlyingMap) {
        putAll(underlyingMap);
    }

    @Override
    public int size() {
        return underlyingMap.size();
    }

    @Override
    public boolean isEmpty() {
        return underlyingMap.isEmpty();
    }

    @Override
    public boolean containsKey(Object o) {
        return underlyingMap.containsKey(o);
    }

    @Override
    public boolean containsValue(Object o) {
        return underlyingMap.containsValue(o);
    }

    @Override
    public Map<K2, V> get(Object o) {
        return underlyingMap.get(o);
    }

    @Override
    public Map<K2, V> put(K1 k1, Map<K2, V> k2VMap) {
        return (k2VMap == null || k2VMap.isEmpty()) ? null : underlyingMap.put(k1, k2VMap);
    }

    @Override
    public Map<K2, V> remove(Object o) {
        return underlyingMap.remove(o);
    }

    /**
     * Puts the contents in a map of maps.
     * Note that existing content stored under an outer key will be overwritten by the provided content.
     * Note that no empty or null inner maps are stored.
     * @param map the content to be added
     */
    @Override
    public void putAll(Map<? extends K1, ? extends Map<K2, V>> map) {
        for(Entry<? extends K1,  ? extends Map<K2, V>> entry : map.entrySet()) {
           if (entry.getValue() != null && ! entry.getValue().isEmpty()) {
               underlyingMap.put(entry.getKey(), entry.getValue());
           }
        }
    }

    @Override
    public void clear() {
        underlyingMap.clear();
    }

    @Override
    public Set<K1> keySet() {
        return underlyingMap.keySet();
    }

    @Override
    public Collection<Map<K2, V>> values() {
        return underlyingMap.values();
    }

    @Override
    public Set<Entry<K1, Map<K2, V>>> entrySet() {
        return underlyingMap.entrySet();
    }

    /**
     * Retrieves a value from a map of maps
     * @param key1 the key for the outer map
     * @param key2 the key for the inner map
     * @return the inner value, or null if none exists
     */
    public V get(K1 key1, K2 key2) {
        Map<K2,V> map = underlyingMap.get(key1);
        return map == null ? null : map.get(key2);
    }

    /**
     * Puts a value in a map of maps.
     * Note that inner maps are created if necessary
     * @param key1 the key for the outer map
     * @param key2 the key for the inner map
     * @param value the value for the inner map
     * @return the former value, or null if none exists
     */
    public V put(K1 key1, K2 key2, V value) {
        Map<K2,V> map = underlyingMap.get(key1);
        if (map == null) {
            map = new HashMap<K2, V>();
            underlyingMap.put(key1, map);
        }
        return map.put(key2, value);
    }

    /**
     * Removes a value from a map of maps.
     * Note that if an inner map gets empty by this operation, it will be removed.
     * @param key1 the key for the outer map
     * @param key2 the key for the inner map
     * @return the former value, or null if none exists
     */
    public V remove(K1 key1, K2 key2) {
        Map<K2,V> map = underlyingMap.get(key1);
        if (map == null) {
            return null;
        } else {
            V result = map.remove(key2);
            if (map.isEmpty()) {
                remove(key1);
            }
            return result;
        }
    }

    /**
     * Returns a list of all values of inner maps to a given inner key
     * @param key2 the inner key
     * @return the list of values
     */
    public List<V> deepGet(K2 key2) {
        List<V> result = new ArrayList<V>();
        for(Map<K2, V> map : underlyingMap.values()) {
            V value = map.get(key2);
            if (value != null) {
                result.add(value);
            }
        }
        return result;
    }

    /**
     * Puts a value in all existing inner maps
     * @param key2 the inner key
     * @param value the inner value
     * @return the list of former values
     */
    public List<V> deepPut(K2 key2, V value) {
        List<V> result = new ArrayList<V>();
        for(Map<K2, V> map : underlyingMap.values()) {
            V v = map.put(key2, value);
            if (v != null) {
                result.add(v);
            }
        }
        return result;
    }

    /**
     * Puts the contents in a map of maps.
     * Note that existing content won't be overwritten.
     * Note that no empty or null inner maps are stored.
     * @param map the content to be added
     */
    public void deepPutAll(Map<? extends K1, ? extends Map<K2, V>> map) {
        for(Entry<? extends K1,  ? extends Map<K2, V>> entry : map.entrySet()) {
            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
                Map<K2, V> innerMap = underlyingMap.get(entry.getKey());
                if (innerMap == null) {
                    underlyingMap.put(entry.getKey(), entry.getValue());
                } else {
                    innerMap.putAll(entry.getValue());
                }
            }
        }
    }


    /**
     * Removes content for a given key from inner maps.
     * Note that if an inner map gets empty by this operation, it will be removed.
     * @param key2 the inner key
     * @return the List of removed values
     */
    public List<V> deepRemove(K2 key2) {
        List<V> result = new ArrayList<V>();
        for(Entry<K1, Map<K2, V>> entry : underlyingMap.entrySet()) {
            Map<K2,V> map = entry.getValue();
            V value = map.remove(key2);
            if (value != null) {
                result.add(value);
            }
            if (map.isEmpty()) {
                remove(entry.getKey());
            }
        }
        return result;
    }

    /**
     * Calculates the total size of all inner maps
     * @return total size
     */
    public int deepSize() {
        int result = 0;
        for(Map<K2,V> map : values()) {
            result += map.size();
        }
        return result;
    }

    /**
     * Returns a set of all inner keys
     * @return set of inner keys
     */
    public Set<K2> deepKeys() {
        Set<K2> result = new HashSet<K2>();
        for(Map<K2,V> map : values()) {
            result.addAll(map.keySet());
        }
        return result;
    }

    /**
     * Returns a list of all inner values
     * @return list of inner values
     */
    public List<V> deepValues() {
        List<V> result = new ArrayList<V>();
        for(Map<K2,V> map : values()) {
            result.addAll(map.values());
        }
        return result;
    }

    /**
     * Returns a list of all inner entries
     * @return list of inner entries
     */
    public List<Entry<K2,V>> deepEntries() {
        List<Entry<K2,V>> result = new ArrayList<Entry<K2,V>>();
        for(Map<K2,V> map : values()) {
            result.addAll(map.entrySet());
        }
        return result;
    }
}
\$\endgroup\$
  • \$\begingroup\$ Hi, could you elaborate on what exactly you are trying to accomplish with your map of maps? By doing so, it would help us comment on your questions towards the end of your description regarding the api. \$\endgroup\$ – Tony R Jul 8 '13 at 18:26
  • \$\begingroup\$ How is this different from Guava's Table? \$\endgroup\$ – jramoyo Jul 9 '13 at 3:39
  • \$\begingroup\$ @jramoyo You are right, I overlooked that one... \$\endgroup\$ – Landei Jul 9 '13 at 6:27
6
\$\begingroup\$

Guava's Table<K1,K2,V> interface looks like exactly what you need for this.

Typically, when you are trying to index on more than one key at a time, you will wind up with something like Map<FirstName, Map<LastName, Person>>, which is ugly and awkward to use. Guava provides a new collection type, Table, which supports this use case for any "row" type and "column" type

There are 4 provided implementations of this interface for different use cases, detailed here.

\$\endgroup\$
6
\$\begingroup\$

Rather than making a map of maps (Map<K1,Map<K2,V>>), I find it often easier to make a map that uses a composite key : Map<Key<K1, K2>, V>.

You could make a generic Key class for this, or make a dedicated class on a per case basis since sometimes this key makes sense business-wise : e.g. Buyer - Seller -> Contract.

Nevertheless, if you use a generic Key class (make sure equals() and hashCode() are implemented properly), you could still have use for a variant of your MapMap, that takes individual K1 and K2 objects and combines them into a key behind the scenes. But I think you'll find that a Key class with a static factory method is easy enough to use :

getting :

V value = map.get(key(k1, k2));

or putting :

map.put(key(k1, k2), value);
\$\endgroup\$
  • 1
    \$\begingroup\$ I have often the "asymmetric" situation that I always know K1, but not necessarily K2, so I would need to scan through the whole map if I would use a compound key. I agree that your version is clearly better if both keys are always known. \$\endgroup\$ – Landei Jul 8 '13 at 18:04
  • \$\begingroup\$ A Comparator on Key and using a SortedMap will allow you to easily find all keys that have a certain K1 value. \$\endgroup\$ – bowmore Jul 8 '13 at 20:09
  • \$\begingroup\$ That sounds good, I'll try it out. \$\endgroup\$ – Landei Jul 8 '13 at 21:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.