views:

5854

answers:

13

Hello!

I need a data structure which behaves like a Map, but uses multiple (differently-typed) keys to access its values.
(Let's don't 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)...

What you would suggest me?

The only thing I can think of is:
Write a class which uses to 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.
Map uses hashcodes of keys and checks for their equality.

A: 

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

marcog
Same here: this would be a problem with equality of keys when searching only for K1 or K2.
ivan_ivanovich_ivanoff
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 Tangora
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
A: 

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
The problem with Pair would be it's equality. If I want to search only by Key1 or only by Key2, such map would never find anything.
ivan_ivanovich_ivanoff
It would not be at all hard to select the set of Pairs satisfying a K1 or K2 constraint, and thence to select the corresponding map elements.
Carl Manaster
A: 

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
+12  A: 

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
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<MetaKey, Map<Object, V>> internally.
Jeremy Huiskamp
A: 

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
A: 

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
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
... and they break interface conventions quite a bit - see MultiMap (and how it would be impossible to make it a Map<K,V>)
Stephen
thanks for the tip
Dima
Actually, commons collections was forked to support generics, see this project here: http://sourceforge.net/projects/collections
Dima
A: 

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
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
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
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
+2  A: 

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
A: 

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.

Kevin Peterson
+9  A: 

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
that's a good idea!
ivan_ivanovich_ivanoff
The only problem with this is if you dont have K1.
Milhous
Milhous: Yes that is a problem with this solution.
Logan Capaldo
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<K1, K2> 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
A: 

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
+9  A: 

Commons-collections provides just what you are looking for: http://commons.apache.org/collections/apidocs/org/apache/commons/collections/map/MultiKeyMap.html

http://commons.apache.org

However that one is not typed.

A typed version can be found at: http://sourceforge.net/projects/collections

This will exactly support your use case:

 MultiKeyMap<k1,k2,...,kn,v> multiMap = ??
Nathan Feger
A: 

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