186

I need a data structure which behaves like a Map, but uses multiple (differently-typed) keys to access its values.
(Let's not be too general, let's say two keys)

Keys are guaranteed to be unique.

Something like:

MyMap<K1,K2,V> ...

With methods like:

getByKey1(K1 key)...
getByKey2(K2 key)...
containsKey1(K1 key)...
containsKey2(K2 key)...

Do you have any suggestions?

The only thing I can think of is:
Write a class which uses two Maps internally.

EDIT Some people suggest me to use a tuple, a pair, or similar as a key for Java's Map, but this would not work for me:
I have to be able, as written above, to search values by only one of the two keys specified.
Maps use hash codes of keys and check for their equality.

nha
  • 17,623
  • 13
  • 87
  • 133
ivan_ivanovich_ivanoff
  • 19,113
  • 27
  • 81
  • 100

27 Answers27

113

Two maps. One Map<K1, V> and one Map<K2, V>. If you must have a single interface, write a wrapper class that implements said methods.

Jeremy Huiskamp
  • 5,186
  • 5
  • 26
  • 19
  • 7
    Er, yeah, this is exactly what you proposed. Two Maps is the way to go. To extend to an arbitrary number of keys, have a method like getByKey(MetaKey mk, Object k) and then a Map> internally. – Jeremy Huiskamp May 04 '09 at 22:21
  • Always wrap the two maps in a class! – Bill K Sep 11 '19 at 21:42
47

Commons-collections provides just what you are looking for: https://commons.apache.org/proper/commons-collections/apidocs/

Looks like now the commons-collections is typed.

A typed version can be found at: https://github.com/megamattron/collections-generic

This will exactly support your use case:

 MultiKeyMap<k1,k2,...,kn,v> multiMap = ??
computingfreak
  • 4,939
  • 1
  • 34
  • 51
Nathan Feger
  • 19,122
  • 11
  • 62
  • 71
  • 1
    I believe using generics (with the alternate collections lib) is only possible if all of your keys are of the same type. `MultiKeyMap` simply does not compile. – gdw2 Aug 16 '12 at 11:42
  • 83
    I think this unswer is wrong. There is no way to get a value from commons MultiKeyMap just by using a second key. You always need to specify both key1 and key2. Why did this answer get so many up votes??? – Dime Jun 25 '14 at 07:02
  • 1
    @Zombies, you did not get my point. There is no way you can solve the initial problem with Commons-collections's MultiKeyMap class. If there is, please, explain it. – Dime Sep 29 '14 at 09:28
  • 3
    @Dime I misunderstood both the question and the answer, but ended up getting what I needed :) – James Hopkin Feb 24 '16 at 13:22
  • 11
    @Dime it got so many upvotes because it is a very useful answer, as many people (like myself) ended up on that question to find that answer. It's just the question that is wrong ;) – Matthieu May 20 '16 at 09:50
  • 1
    This solution is more like a complex (composite) unique key in DB. Where two keys are required to identify particular object. Still, as @Dime pointed out, one key is not sufficient to perform any operation on value. – suhas0sn07 Nov 28 '18 at 07:15
44

I'm still going suggest the 2 map solution, but with a tweest

Map<K2, K1> m2;
Map<K1, V>  m1;

This scheme lets you have an arbitrary number of key "aliases".

It also lets you update the value through any key without the maps getting out of sync.

Logan Capaldo
  • 39,555
  • 5
  • 63
  • 78
  • 21
    The only problem with this is if you dont have K1. – Milhous May 05 '09 at 01:59
  • 1
    Milhous: Yes that is a problem with this solution. – Logan Capaldo May 05 '09 at 02:17
  • Updating is a problem in my response as well. Eg. you store value "1" with K1=A and K2=B, then you store value "1" with K1=C and K2=D. Now you try to set value = "2" where K1=A. How do you know which K2 to use? Logan, your solution works, but you might also want to keep a Map as well. This is probably getting a bit theoretical though, as it doesn't look like the original question takes updates into account. – Jeremy Huiskamp May 05 '09 at 04:29
  • This requires two look ups for every value access, which is a 100 % performance penalty. – ceving Mar 15 '13 at 15:40
  • @Milhous I'm not sure why updating problem as described by you is applicable to Logan's solution? Here we don't need to update two maps as value updation just need to be done in map m1 e.g. if user says put(k,v) then if k,k1 entry exists in map m2 then we will put k1,v in map m2, else we will put k,v in map m1 as new entry. Also, if user request to put(k2,v) and k1,v entry already exists in map m1 then we will have to store k2,k1 in map m2 instead of storing k2,v in map m1. – sactiw Oct 31 '13 at 08:41
  • Also, storing k,k entry in map m2 for every new entry k,v getting stored in map m1 will make searching cleaner i.e. look in map m2 first and if entry exists then fetch value from map m1 – sactiw Oct 31 '13 at 08:41
  • great answer, this way I avoid having the value in 2 separate maps (which takes more space) and also if value is updated I have to update it only in one map. – damian Jan 21 '15 at 15:31
  • Little late to the party here, but this doesn't work for removing(K1), since you have to also remove K2 from M2. Or maybe I am mistaken? – Nathan Mar 10 '17 at 20:19
  • Using this vs. two maps really depends on how big your primary map is (both in terms of number of items and bytes per item). By design maps are very efficient to lookup a value by key, no need to loop over items like a list. If the primary map is very small in size, it's fine to duplicate the map without impact to RAM in order to save a lookup. If it's large, this solution is better since it has less impact on RAM (key2-to-key1 map bytes << key1-to-value map bytes). – akerra Aug 29 '21 at 13:47
40

Yet another solution is to use Google's Guava

import com.google.common.collect.Table;
import com.google.common.collect.HashBasedTable;

Table<String, String, Integer> table = HashBasedTable.create();

The usage is really simple:

String row = "a";
String column = "b";
int value = 1;

if (!table.contains(row, column)) {
    table.put(row, column, value);
}

System.out.println("value = " + table.get(row, column));

The method HashBasedTable.create() is basically doing something like this:

Table<String, String, Integer> table = Tables.newCustomTable(
        Maps.<String, Map<String, Integer>>newHashMap(),
        new Supplier<Map<String, Integer>>() {
    public Map<String, Integer> get() {
        return Maps.newHashMap();
    }
});

if you're trying to create some custom maps, you should go for the second option (as @Karatheodory suggests) otherwise you should be fine with the first one.

Tombart
  • 30,520
  • 16
  • 123
  • 136
  • 2
    In the example above, it would be better to use Guava's default factory for hash-based tables: `HashBasedTable.create()`. The `newCustomTable()` method should only be used for truly custom maps. – Karatheodory Jun 03 '14 at 09:29
21

What about you declare the following "Key" class:

public class Key {
   public Object key1, key2, ..., keyN;

   public Key(Object key1, Object key2, ..., Object keyN) {
      this.key1 = key1;
      this.key2 = key2;
      ...
      this.keyN = keyN;
   }

   @Override   
   public boolean equals(Object obj) {
      if (!(obj instanceof Key))
        return false;
      Key ref = (Key) obj;
      return this.key1.equals(ref.key1) && 
          this.key2.equals(ref.key2) &&
          ...
          this.keyN.equals(ref.keyN)
   }

    @Override
    public int hashCode() {
        return key1.hashCode() ^ key2.hashCode() ^ 
            ... ^ keyN.hashCode();
    }

}

Declaring the Map

Map<Key, Double> map = new HashMap<Key,Double>();

Declaring the key object

Key key = new Key(key1, key2, ..., keyN)

Filling the map

map.put(key, new Double(0))

Getting the object from the map

Double result = map.get(key);
Carlo Espino
  • 1,354
  • 1
  • 15
  • 21
Guigon
  • 235
  • 2
  • 2
  • 1
    this approach is more powerful than it looks like on first sight. you may even simply use an `Object[]` as key – benez Dec 20 '16 at 13:44
  • Are the ellipsis arguments actual ellipses? Or are you just indicating that the user is to specify 1, 2 or up to N when they create the class. In other words, this is not a class that will handle a variable number of keys is it? – Nicholas Hamilton Oct 01 '17 at 06:16
  • 3
    OP doesn't want to combine keys, but use either seperately. – Adam May 04 '18 at 15:29
  • How does this work , when you want to lookup by just key1 ? – MithunS Sep 15 '18 at 18:42
  • @MithunS If I'm understanding this answer correctly, the effect is that a given value will appear in the map twice (or more), once for each key that points to that value. In this way a single map can used for both keys. Searching the map for either key will return the value. A concise form would declare Map map ... Retrieval would be simple: map.get(someKeyForValue1), map.get(otherKeyForValue1) or map.contains(otherKeyForValue1). It does still have close to the same memory usage as the two map approach in another answer, but might be more straight-forward. – Iain Oct 02 '19 at 23:46
7

Proposal, as suggested by some answerers:

public interface IDualMap<K1, K2, V> {

    /**
    * @return Unmodifiable version of underlying map1
    */
    Map<K1, V> getMap1();

    /**
    * @return Unmodifiable version of underlying map2
    */
    Map<K2, V> getMap2();

    void put(K1 key1, K2 key2, V value);

}

public final class DualMap<K1, K2, V>
        implements IDualMap<K1, K2, V> {

    private final Map<K1, V> map1 = new HashMap<K1, V>();

    private final Map<K2, V> map2 = new HashMap<K2, V>();

    @Override
    public Map<K1, V> getMap1() {
        return Collections.unmodifiableMap(map1);
    }

    @Override
    public Map<K2, V> getMap2() {
        return Collections.unmodifiableMap(map2);
    }

    @Override
    public void put(K1 key1, K2 key2, V value) {
        map1.put(key1, value);
        map2.put(key2, value);
    }
}
ivan_ivanovich_ivanoff
  • 19,113
  • 27
  • 81
  • 100
3

I recommend something like this:

    public class MyMap {

      Map<Object, V> map = new HashMap<Object, V>();


      public V put(K1 key,V value){
        return map.put(key, value);
      }

      public V put(K2 key,V value){
        return map.put(key, value);
      }

      public V get(K1 key){    
        return map.get(key);
      }

      public V get(K2 key){    
        return map.get(key);
      }

      //Same for conatains

    }

Then you can use it like:
myMap.put(k1,value) or myMap.put(k2,value)

Advantages: It is simple, enforces type safety, and doesn't store repeated data (as the two maps solutions do, though still store duplicate values).
Drawbacks: Not generic.

user454322
  • 7,300
  • 5
  • 41
  • 52
  • This is actually the best answer for the question as asked, although I think the intent would generally be to supply two keys at the same time so it needs a method put(K1 key, K2 key, V value). I believe It could also be made generic by adding to the class definition, internally using Object (or better yet ?) should be fine. – Bill K Sep 11 '19 at 22:57
3

Why not just drop the requirement that the key has to be a specific type, i.e. just use Map<Object,V>.

Sometimes generics just isn't worth the extra work.

Mike Kucera
  • 2,003
  • 1
  • 14
  • 18
2

Both MultiMap or MultiKeyMap from Commons or Guava will work.

However, a quick and simple solution could be to extend Map class buy handling a composite key yourself, considering that keys are of primitive type.

AlexVPerl
  • 7,652
  • 8
  • 51
  • 83
  • 6
    -1 This does not answer the question. The asker does not need a compound key, but two distinct single keys that point to the same object but are managed as one entry so to say. Read the question again. `MultiKeyMap` is nice but only deals with compound keys. – Frederic Leitenberger Feb 10 '17 at 17:24
  • 1
    Didn't answer OP's question, but did answer mine... – Sarah Messer Jun 15 '17 at 20:06
  • 1
    The OP doesn't want to map using keyA and keyB, rather keyA or keyB – Adam May 04 '18 at 15:26
2

sol: cancatenate both keys and make a final key, use this as key.

for key values ,

concatenate ket-1, and key-2 with a come " , " in beetween, use this as original key.

key = key-1 + "," + key-2;

myMap.put(key,value);

similarly while retriving values.

amjed
  • 39
  • 1
2

I can see the following approaches:

a) Use 2 different maps. You can wrap them in a class as you suggest, but even that might be an overkill. Just use the maps directly: key1Map.getValue(k1), key2Map.getValue(k2)

b) You can create a type-aware key class, and use that (untested).

public class Key {
  public static enum KeyType { KEY_1, KEY_2 }

  public final Object k;
  public final KeyType t;

  public Key(Object k, KeyType t) {
    this.k = k;
    this.t= t;
  }

  public boolean equals(Object obj) {
    KeyType kt = (KeyType)obj;
    return k.equals(kt.k) && t == kt.t;
  }

  public int hashCode() {
   return k.hashCode() ^ t.hashCode();
  }
}

By the way, in a lot of common cases the space of key1 and the space of key2 do not intersect. In that case, you don't actually need to do anything special. Just define a map that has entries key1=>v as well as key2=>v

ykaganovich
  • 14,736
  • 8
  • 59
  • 96
  • Could you please comment on consistency of hashcode vs equals of your proposal b)? And why do you say 'overkill' for a)? Thank you – ivan_ivanovich_ivanoff May 04 '09 at 22:43
  • hashcode seems consistent with equals to me in that any time two objects are equal their hashcodes will be equal. Not that the other way is not a requirement, although a desirable property of the hash function. Using primes is another option as per "Effective Java", e.g. k.hashCode() * 17 + t.hashCode(). More info: http://www.ibm.com/developerworks/java/library/j-jtp05273.html – ykaganovich May 04 '09 at 23:46
  • To expand on the "overkill" statement, multiKeyMap.getValueByKey1(k) doesn't seem any cleaner to me (subjectively) than key1Map.getValue(k), either in terms of verbosity or the amount of semantic information conveyed. I don't know your specific usecase though. – ykaganovich May 05 '09 at 00:38
2

all multy key's probably fail, cause the put([key1, key2], val) and the get([null, key2]) end up using the equals of [key1, key2] and [null, key2]. If the backing map doesnt contains hash buckets per key then lookups are real slow to.

i think the way to go is using a index decorator (see the key1, key2 examples above) and if the extra index key's are properties of the stored value you can use the property name's and reflection to build the secondairy maps when you put(key, val) and add an extra method get(propertyname, propertyvalue) to use that index.

the return type of the get(propertyname, propertyvalue) could be a Collection so even none unique key's are indexed....

bram
  • 21
  • 1
2

I created this to solve a similar issue.

Datastructure

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

public class HashBucket {
    HashMap<Object, ArrayList<Object>> hmap;

    public HashBucket() {
        hmap = new HashMap<Object, ArrayList<Object>>();
    }

    public void add(Object key, Object value) {
        if (hmap.containsKey(key)) {
            ArrayList al = hmap.get(key);
            al.add(value);
        } else {
            ArrayList al = new ArrayList<Object>();
            al.add(value);
            hmap.put(key, al);
        }
    }

    public Iterator getIterator(Object key) {
        ArrayList al = hmap.get(key);
        return hmap.get(key).iterator();

    }

}

Retrieve a value:

(Note* Cast the Object back to the inserted type. In my case it was my Event Object)

    public Iterator getIterator(Object key) {
        ArrayList al = hmap.get(key);
        if (al != null) {
            return hmap.get(key).iterator();
        } else {
            List<Object> empty = Collections.emptyList();
            return empty.iterator();
        }

    }

Inserting

Event e1 = new Event();
e1.setName("Bob");
e1.setTitle("Test");
map.add("key",e1);
Jon Polaski
  • 134
  • 2
  • 3
1

I used such implementation for multiple key objects. It allows me to use innumerable number of keys for map. It's scaleable and quite simple. But it has limitations: keys are ordered according to order of arguments in constructor and it would not work with 2D arrays, because of using Arrays.equals(). To fix it - you could use Arrays.deepEquals();

Hope it will help you. If you know any reason why it could not be used as a solution for such issues - please, let me know!

public class Test {

    private static Map<InnumerableKey, Object> sampleMap = new HashMap<InnumerableKey, Object>();

    private static class InnumerableKey {

        private final Object[] keyParts;

        private InnumerableKey(Object... keyParts) {
            this.keyParts = keyParts;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof InnumerableKey)) return false;

            InnumerableKey key = (InnumerableKey) o;

            if (!Arrays.equals(keyParts, key.keyParts)) return false;

            return true;
        }

        @Override
        public int hashCode() {
            return keyParts != null ? Arrays.hashCode(keyParts) : 0;
        }
    }

    public static void main(String... args) {
        boolean keyBoolean = true;
        double keyDouble = 1d;
        Object keyObject = new Object();

        InnumerableKey doubleKey = new InnumerableKey(keyBoolean, keyDouble);
        InnumerableKey tripleKey = new InnumerableKey(keyBoolean, keyDouble, keyObject);

        sampleMap.put(doubleKey, "DOUBLE KEY");
        sampleMap.put(tripleKey, "TRIPLE KEY");

        // prints "DOUBLE KEY"
        System.out.println(sampleMap.get(new InnumerableKey(true, 1d)));
        // prints "TRIPLE KEY"
        System.out.println(sampleMap.get(new InnumerableKey(true, 1d, keyObject)));
        // prints null
        System.out.println(sampleMap.get(new InnumerableKey(keyObject, 1d, true)));
    }
}
Gadget
  • 474
  • 1
  • 5
  • 12
1

How about something like this:

His statement says that keys are Unique, so saving the same value objects against different keys is quite possible and when you send any key matching the said value, we would be able to get back to the value object.

See code below:

A value Object Class,

    public class Bond {
    public Bond() {
        System.out.println("The Name is Bond... James Bond...");
    }
    private String name;
    public String getName() { return name;}
    public void setName(String name) { this.name = name; }
}

public class HashMapValueTest {

    public static void main(String[] args) {

        String key1 = "A";
        String key2 = "B";
        String key3 = "C";

        Bond bond = new Bond();
        bond.setName("James Bond Mutual Fund");

        Map<String, Bond> bondsById = new HashMap<>();

        bondsById.put(key1, bond);
        bondsById.put(key2, bond);
        bondsById.put(key3, bond);

        bond.setName("Alfred Hitchcock");

        for (Map.Entry<String, Bond> entry : bondsById.entrySet()) {
            System.out.println(entry.getValue().getName());
        }

    }

}

The result is:

The Name is Bond... James Bond...

Alfred HitchCock

Alfred HitchCock

Alfred HitchCock
umlcat
  • 4,091
  • 3
  • 19
  • 29
1

If keys are unique then there is no need for 2 maps, map of maps, mapOfWhateverThereIs. There needs to be only 1 single map and just a simple wrapper method that would put your keys and value into that map. Example:

Map<String, String> map = new HashMap<>();

public void addKeysAndValue(String key1, String key2, String value){
    map.put(key1, value);
    map.put(key2, value);
}

public void testIt(){
    addKeysAndValue("behemoth", "hipopotam", "hornless rhino");
}

Then use your map as you normally would. You don't even need those fancy getByKeyN and containsKeyN.

VasiliyL
  • 999
  • 8
  • 11
  • 1
    this is a really elegant (partial) solution, but the keys could be from different types (as the question states "I need a data structure which behaves like a Map, but uses multiple **(differently-typed)** keys to access its values.") – Alexander Mihailov Dec 11 '19 at 14:24
  • Strangely enough I didn't see your reply back in 2019. For differently-typed keys I would use... an Object? Like, Map? – VasiliyL Nov 11 '21 at 14:40
  • 1
    This is really an elegant solution! For keys with different types, find way to convert them all to String with some prefix/suffix/transformation – Kshitij Patil Dec 07 '22 at 13:46
1

Sounds like a Python tuple. Following in that spirit, you can create an immutable class of your own devising that implements Comparable and you'll have it.

duffymo
  • 305,152
  • 44
  • 369
  • 561
0

I would suggest the structure

Map<K1, Map<K2, V>>

although searching for the second key might not be efficient

kon psych
  • 626
  • 1
  • 11
  • 26
0

Another possile solution providing the possibility of more complicated keys can be found here: http://insidecoffe.blogspot.de/2013/04/indexable-hashmap-implementation.html

Jan Wiemer
  • 31
  • 2
0

How about using a trie data structure ?

http://en.wikipedia.org/wiki/Trie

The root of the trie will by blank. The first level siblings will be your primary keys of the map, the second level siblings will be your secondary keys and the third level will be the terminal nodes which will have the value along will null to indicate termination of that branch. You can also add more than two keys using the same scheme.

Look up is simple DFS.

0

A dirty and a simple solution, if you use the maps just for sorting lets say, is to add a very small value to a key until the value does not exist, but do not add the minimum (for example Double.MIN_VALUE) because it will cause a bug. Like I said, this is a very dirty solution but it makes the code simpler.

3xCh1_23
  • 1,491
  • 1
  • 20
  • 39
0

Define a class that has an instance of K1 and K2. Then use that as class as your key type.

moinudin
  • 134,091
  • 45
  • 190
  • 216
  • Same here: this would be a problem with equality of keys when searching only for K1 or K2. – ivan_ivanovich_ivanoff May 04 '09 at 22:14
  • In C++, you could override the equality test to be true if either member is equal, rather than the default that's true only if both are equal. Is that not possible in Java? – Charlie May 04 '09 at 22:16
  • 2
    It is perfectly possible, but it might not have the desired effect; if your map implementation relies upon the object's hash code, as in a HashMap, then that screws everything to hell. – Rob May 05 '09 at 00:56
0

See Google Collections. Or, as you suggest, use a map internally, and have that map use a Pair. You'll have to write or find Pair<>; it's pretty easy but not part of the standard Collections.

Carl Manaster
  • 39,912
  • 17
  • 102
  • 155
0

Sounds like your solution is quite plausible for this need, I honestly don't see a problem with it if your two key types are really distinct. Just makes ure you write your own implementation for this and deal with synchronization issues if needed.

Uri
  • 88,451
  • 51
  • 221
  • 321
0

If you intend to use combination of several keys as one, then perhaps apache commnons MultiKey is your friend. I don't think it would work one by one though..

Dima
  • 4,068
  • 4
  • 38
  • 47
  • I would advice EVERYONE against Apache Collections. Since nearly 6 years we have Java 5 with its generic types, which Apache Collections doesn't support. – ivan_ivanovich_ivanoff May 04 '09 at 22:23
  • ... and they break interface conventions quite a bit - see MultiMap (and how it would be impossible to make it a Map) – Stephen May 05 '09 at 00:40
  • 3
    Actually, commons collections was forked to support generics, see this project here: http://sourceforge.net/projects/collections – Dima May 25 '09 at 16:48
  • @ivan_ivanovich_ivanoff: See [Is there a viable generic alternative to apache.commons.collections.CollectionUtils?](http://stackoverflow.com/q/7015739). – Martin Schröder Dec 18 '15 at 09:28
0

Depending on how it will be used, you can either do this with two maps Map<K1, V> and Map<K2, V> or with two maps Map<K1, V> and Map<K2, K1>. If one of the keys is more permanent than the other, the second option may make more sense.

Logan Capaldo
  • 39,555
  • 5
  • 63
  • 78
Kevin Peterson
  • 7,189
  • 5
  • 36
  • 43
0

It would seem to me that the methods you want in your question are supported directly by Map. The one(s) you'd seem to want are

put(K1 key, K2 key, V value)
put(K1 key, V value)
put(K2 key, V value)

Note that in map, get() and containsKey() etc all take Object arguments. There's nothing stopping you from using the one get() method to delegate to all the composite maps that you combine (as noted in your question and other answers). Perhaps you'd need type registration so you don't get class casting problems (if they're special + implemented naively.

A typed based registration would also allow you to retrieve the "correct" map to be used:

Map<T,V> getMapForKey(Class<T> keyClass){
  //Completely naive implementation - you actually need to 
  //iterate through the keys of the maps, and see if the keyClass argument
  //is a sub-class of the defined map type.  And then ordering matters with 
  //classes that implement multiple interfaces...
  Map<T,V> specificTypeMap = (Map<T,V) maps.get(keyClass);
  if (specificTypeMap == null){
     throw new IllegalArgumentException("There is no map keyed by class" + keyClass);
  }
  return maps.get(keyClass);
}

V put(Object key, V value) {
  //This bit requires generic suppression magic - but 
  //nothing leaves this class and you're testing it right? 
  //(You can assert that it *is* type-safe)
  Map map = getMapForKey(key.getClass());
  map.put(object, key);
}

void put(Object[] keys, V value) { //Or put(V value, Object ... keys)
   //Might want to catch exceptions for unsupported keys and log instead?
   .....
}

Just some ideas...

Stephen
  • 19,488
  • 10
  • 62
  • 83