2

I want to store values against keys.

The keys can be compound, with up to two components per key.

I would like to map

Compound key {A,null} to Value 1

This Value 1 will be the default value for all lookups for keys having A as their first component, UNLESS an exact matching key has been added to the map.

So if I add to the map

Compound key {A,Z} having Value 2

When I do a look up, I would like ALL look ups of type {A,*} to return 1, so {A,F} returns 1 as it not specified so falls back to the default. The exception to this would be {A,Z}, which returns 2 as it is explicitly specified.

I can do this from first principles, by checking that an exact key (two components) match exists before checking on a one component match.

However, is there an existing collection that can do this for me?

What if I have an arbitrary number of components?

user1717259
  • 2,717
  • 6
  • 30
  • 44

4 Answers4

2

"What if I have an arbitrary number of components?"

Well then suppress the heck out of some warnings and go old school with well hidden raw maps.

import java.util.HashMap;
import java.util.Map;


/**
 *  Map lookup with arbitrary number of keys, as set with first use of lay()
 *  Missing keys map to null if null key exists
 */
public class MultiKeyMap<K, V>
{
    int expectedNumberOfKeys = -1;
    V value;

    @SuppressWarnings("rawtypes")
    Map<K, Map> topMap = new HashMap<K, Map>();

    /** Map to value from keys */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public V lay(V value, K... keys)
    {
        if (keys == null)
        {
            //there are no keys.
            expectedNumberOfKeys = 0;
            V oldValue = this.value;
            this.value = value; 
            return oldValue;
        }

        if (expectedNumberOfKeys != -1 && expectedNumberOfKeys != keys.length)
        {
            throw new IllegalArgumentException("Expecting " + expectedNumberOfKeys + " keys.  Was " + keys.length );
        }

        expectedNumberOfKeys = keys.length;

        Map<K, Map> currentMap = topMap; 

        //all but last key
        for(int i = 0; i < keys.length - 1; i++)
        {
            K key = keys[i];

            currentMap = linkToNextMap(currentMap, key);
        }

        //last key
        V oldValue = ((Map<K,V>)currentMap).put(keys[keys.length - 1], value); 
        return oldValue;

    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    Map<K,Map> linkToNextMap(Map<K,Map> map, K key)
    {
        Map<K, Map> nextMap = null;

        if ( ! map.containsKey(key) )
        {
            map.put(key, new HashMap<K, Map>() );
        }

        nextMap = map.get(key);

        return nextMap;
    }

    /** 
     * Get value maped from keys.  Must include as many keys as laid down.  
     * Keys not found are taken as null keys 
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public V get(K... keys)
    {
        if (keys == null)
        {
            return value;
        }

        //System.out.println(topMap+" <- topMap");//TODO remove

        if (expectedNumberOfKeys == -1)
        {
            return null;
        }

        if (expectedNumberOfKeys == 0)
        {
            return value;
        }

        if (expectedNumberOfKeys != keys.length)
        {
            throw new IllegalArgumentException("Expecting " + expectedNumberOfKeys + " keys.  Was " + keys.length );
        }

        Map<K, Map> currentMap = topMap;

        //All but last key
        for(int i = 0; i < keys.length - 1; i++)
        {
            currentMap = (Map) getDefault(currentMap, keys[i]);
        }

        //Last key
        V result = (V) getDefault(currentMap, keys[keys.length - 1]);

        return result;
    }

    @SuppressWarnings("rawtypes")
    Object getDefault(Map map, K key)
    {
        Object result = null;

        if (map != null)
        {

            //Use default key (null) if not found
            if ( ! map.containsKey(key) )
            {
                key = null;
            }

            result = map.get(key);
        }

        return result;
    }

    public static void main(String[] args)
    {
        //Build {null={D=4, null=3}, A={null=1, Z=2}} 
        MultiKeyMap<String, Integer> map2 = new MultiKeyMap<String, Integer>();
        map2.lay(1, "A", null);
        map2.lay(2, "A", "Z");
        map2.lay(3, null, null);
        map2.lay(4, null, "D");
        System.out.println(map2.get("A", null)); //1        
        System.out.println(map2.get("A", "Z"));  //2
        System.out.println(map2.get("A", "F"));  //1 F not found so treating as null
        System.out.println(map2.get(null, null));//3 
        System.out.println(map2.get(null, "D")); //4
        System.out.println(map2.get("F", "D"));  //4 F not found so treating as null
        System.out.println();

        //Build {null={D={C=4}, null={C=3}}, A={null={B=1}, Z={B=2}}} 
        MultiKeyMap<String, Integer> map3 = new MultiKeyMap<String, Integer>();
        map3.lay(1, "A", null, "B");
        map3.lay(2, "A", "Z", "B");
        map3.lay(3, null, null, "C");
        map3.lay(4, null, "D", "C");
        System.out.println(map3.get("A", null, "B")); //1   
        System.out.println(map3.get("A", "Z", "B"));  //2
        System.out.println(map3.get("A", "F", "B"));  //1 F not found so treating as null
        System.out.println(map3.get(null, null, "C"));//3
        System.out.println(map3.get(null, "D", "C")); //4
        System.out.println(map3.get("F", "D", "C"));  //4 F not found so treating as null
    }   
}

Displays:

1
2
1
3
4
4

1
2
1
3
4
4

I know no one is going to vote for this but I couldn't sleep until I got it out of my head.

Good night SO.

candied_orange
  • 7,036
  • 2
  • 28
  • 62
1

For things like this I use Map<String, Map<String, Integer>> You can nest that as deep as you like.

Turns out this works fine with nulls:

    Map<String, Map<String, Integer>> mapOfMap = new HashMap<String, Map<String, Integer>>();

    //Make one of these for every first key
    Map<String, Integer> mapOfInt = new HashMap<String, Integer>(); 

    mapOfInt.put(null, 1); 
    mapOfInt.put("Z", 2); 

    mapOfMap.put("A", mapOfInt);


    System.out.println(mapOfMap.get("A").get(null));
    System.out.println(mapOfMap.get("A").get("Z"));

Displays:

1
2

If you want a class to hide the details of all that (and I wouldn't blame you) try this:

import java.util.HashMap;
import java.util.Map;

public class DoubleKeyMap<K1, K2, V>
{
    Map<K1, Map<K2, V>> mapOfMap;

    public void put(K1 key1, K2 key2, V value)
    {
        if (mapOfMap == null)
        {
            mapOfMap = new HashMap<K1, Map<K2, V>>();
        }

        if ( ! mapOfMap.containsKey(key1) )
        {
            mapOfMap.put(key1, new HashMap<K2, V>() );
        }

        mapOfMap.get(key1).put(key2, value);

    }

    public V get(K1 key1, K2 key2)
    {
        if ( ! mapOfMap.containsKey(key1) )
        {
            key1 = null;
        }
        if ( ! mapOfMap.get(key1).containsKey(key2) )
        {
            key2 = null;
        }
        return mapOfMap.get(key1).get(key2);
    }

    public static void main(String[] args)
    {
        DoubleKeyMap<String, String, Integer> bigMap = new DoubleKeyMap<String, String,Integer>();
        bigMap.put("A", null, 1);
        bigMap.put("A", "Z", 2);

        System.out.println( bigMap.get("A", null) );
        System.out.println( bigMap.get("A", "Z") );
        System.out.println( bigMap.get("A", "F") );
    }
}

Displays:

1
2
1
candied_orange
  • 7,036
  • 2
  • 28
  • 62
0

I think the following requirement is impossible to fulfill with a simple Map :

When I do a look up, I would like ALL look ups of type {A,*} to return 1, with the exception of {A,Z}, which returns 2.

Indeed, imagine that it is possible and that map.get(new Compound(A,B)) returns the value associated with new Compound(A,null). Then, it would mean that new Compound(A,B) and new Compound(A,null) would have the same hashcode and are equal. Now, if I do map.put(new Compound(A,B),3), it is going to overwrite the value associated with new Compound(A,null), which is not what you want.

You need to implement your own type to do this. The implementation could wrap a Map<Compound<T1,T2>,Integer> for compound values with a non-null second component and a Map<T1,Integer> for the others. You would first look up into the first map, and if you don't find a match you try with the second one. I don't see any way to extend it to any number of components in a smart way.

Dici
  • 25,226
  • 7
  • 41
  • 82
0

Haven't thoroughly thought of any potential implications to other methods, but what you could do is overwrite Hashmap.get()

public V get(Object key) { V ret = super.get(key); if (ret==null){ //safe cast to Compound key Compound c=...; ret = super.get(new Compound(c.firstPart, null)); } return ret; } You can create a Compound key that allows more constituents and you can also define the order that it does so. e.g. Compound.getGeneralizedKey() that return a new Compound nullifying constituents in the order of your search.

Having said that, note that each of your get operations will now be an O(n) operation. i.e. For each get you will need to execute get n times.

PS. If you don't need to implement the Map interface, it would be better if you overrode Hashmap adding a safer method like getRecursive(Compound key)

Ioannis Deligiannis
  • 2,679
  • 5
  • 25
  • 48