103

I have data that is organized in kind of a "key-key" format, rather than "key-value". It's like a HashMap, but I will need O(1) lookup in both directions. Is there a name for this type of data structure, and is anything like this included in Java's standard libraries? (or maybe Apache Commons?)

I could write my own class that basically uses two mirrored Maps, but I'd rather not reinvent the wheel (if this already exists but I'm just not searching for the right term).

Jonik
  • 80,077
  • 70
  • 264
  • 372
Kip
  • 107,154
  • 87
  • 232
  • 265

7 Answers7

113

There is no such class in the Java API. The Apache Commons class you want is going to be one of the implementations of BidiMap.

As a mathematician, I would call this kind of structure a bijection.

dARKpRINCE
  • 1,538
  • 2
  • 13
  • 22
uckelman
  • 25,298
  • 8
  • 64
  • 82
81

In addition to Apache Commons, Guava also has a BiMap.

Abdull
  • 26,371
  • 26
  • 130
  • 172
ColinD
  • 108,630
  • 30
  • 201
  • 202
  • thanks for the info! i'm sticking with apache for the time being though (unless there are some good reasons not to?) – Kip Nov 03 '09 at 21:10
  • I can't offer a good comparison to apache collections, but google collections does have a lot of nice stuff that I think would make it worth looking in to. – ColinD Nov 03 '09 at 21:25
  • 17
    One advantage of Google Collections is that it has generics whereas Commons Collections does not. – Mark Nov 03 '09 at 21:38
  • 3
    For comparison of the two libs, see the quotes in this answer: http://stackoverflow.com/questions/787446/is-there-a-java-1-5-equivalent-to-the-predicatet-methods-in-net/787459#787459 (and the original interview). That's biased towards Google, for obvious reasons, but even so I think it's safe to say you're better off with Google Collections nowadays. – Jonik Nov 05 '09 at 18:21
  • 1
    The BiMap link is broken. Please use [this one](https://github.com/google/guava/wiki/NewCollectionTypesExplained#bimap). – Mahsa2 Aug 20 '16 at 17:48
  • Apache Commons Collections added generics support for 4.0, released in 2013 – lmsurprenant Jun 16 '21 at 15:24
21

Here is a simple class I used to get this done (I did not want to have yet another third party dependency). It does not offer all features available in Maps but it is a good start.

    public class BidirectionalMap<KeyType, ValueType>{
        private Map<KeyType, ValueType> keyToValueMap = new ConcurrentHashMap<KeyType, ValueType>();
        private Map<ValueType, KeyType> valueToKeyMap = new ConcurrentHashMap<ValueType, KeyType>();

        synchronized public void put(KeyType key, ValueType value){
            keyToValueMap.put(key, value);
            valueToKeyMap.put(value, key);
        }

        synchronized public ValueType removeByKey(KeyType key){
            ValueType removedValue = keyToValueMap.remove(key);
            valueToKeyMap.remove(removedValue);
            return removedValue;
        }

        synchronized public KeyType removeByValue(ValueType value){
            KeyType removedKey = valueToKeyMap.remove(value);
            keyToValueMap.remove(removedKey);
            return removedKey;
        }

        public boolean containsKey(KeyType key){
            return keyToValueMap.containsKey(key);
        }

        public boolean containsValue(ValueType value){
            return keyToValueMap.containsValue(value);
        }

        public KeyType getKey(ValueType value){
            return valueToKeyMap.get(value);
        }

        public ValueType get(KeyType key){
            return keyToValueMap.get(key);
        }
    }
GETah
  • 20,922
  • 7
  • 61
  • 103
  • 6
    You will significantly improve performance of containsValue() by changing it to return valueToKeyMap.containsKey(value) – JT. Apr 23 '13 at 00:46
  • 1
    I would not use this map as it is currently because the bi-directionality breaks if a key (or value) is re-added with a different value (or key), which would be valid usage to update a key IMO. – Qw3ry Feb 07 '17 at 13:31
10

If no collisions occur, you can always add both directions to the same HashMap :-)

rsp
  • 23,135
  • 6
  • 55
  • 69
  • 6
    @Kip: Why? In some contexts this is a perfectly legitimate solution. So would be having two hash maps. – Lawrence Dol Nov 03 '09 at 23:38
  • 8
    no, it's an ugly, fragile hack. it requires maintenance of the bi-directional property on every get() and put(), and it could be passed to other methods that modify the map without even knowing about the bi-directional property. maybe it'd be okay as a local variable inside a method that isn't passed anywhere, or if it was made unmodifiable immediately after creation. but even then, it's fragile (someone comes along and tweaks that function and breaks bidirectionality in a way that will not always immediately show itself to be a problem) – Kip Nov 04 '09 at 01:22
  • 1
    @Kip, I agree that such a usage should be kept internal to the class using that map, but your last remark only holds true if the corresponding JUnit tests are sloppy :-) – rsp Nov 04 '09 at 09:32
  • If I may pose a very valid use of such an implementation imagine needing a map for decoding/encoding opcodes of assembly language instructions here you will never change the state of the map also, in one direction the keys are instructions strings the other binary values. So you should never have conflicts. – M.K Dec 21 '13 at 19:28
  • For a small scale lookup purpose, that hack solves my problem. – milkersarac Feb 15 '16 at 14:00
  • I just used this – Stefan Reich Oct 16 '17 at 16:32
9

Here my 2 cents.

Or you can use a simple method with generics. Piece of cake.

public static <K,V> Map<V, K> invertMap(Map<K, V> toInvert) {
    Map<V, K> result = new HashMap<V, K>();
    for(K k: toInvert.keySet()){
        result.put(toInvert.get(k), k);
    }
    return result;
}

Of course you must have a map with unique values. Otherwise, one of them will be replaced.

Fulvius
  • 511
  • 6
  • 15
2

Inspired by GETah's answer I decided to write something similar by myself with some improvements:

  • The class is implementing the Map<K,V>-Interface
  • The bidirectionality is really guaranteed by taking care of it when changing a value by a put (at least I hope to guarantee it hereby)

Usage is just like a normal map, to get a reverse view on the mapping call getReverseView(). The content is not copied, only a view is returned.

I'm not sure this is totally fool-proof (actually, it's probably not), so feel free to comment if you notice any flaws and I'll update the answer.

public class BidirectionalMap<Key, Value> implements Map<Key, Value> {

    private final Map<Key, Value> map;
    private final Map<Value, Key> revMap;

    public BidirectionalMap() {
        this(16, 0.75f);
    }

    public BidirectionalMap(int initialCapacity) {
        this(initialCapacity, 0.75f);
    }

    public BidirectionalMap(int initialCapacity, float loadFactor) {
        this.map = new HashMap<>(initialCapacity, loadFactor);
        this.revMap = new HashMap<>(initialCapacity, loadFactor);
    }

    private BidirectionalMap(Map<Key, Value> map, Map<Value, Key> reverseMap) {
        this.map = map;
        this.revMap = reverseMap;
    }

    @Override
    public void clear() {
        map.clear();
        revMap.clear();
    }

    @Override
    public boolean containsKey(Object key) {
        return map.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return revMap.containsKey(value);
    }

    @Override
    public Set<java.util.Map.Entry<Key, Value>> entrySet() {
        return Collections.unmodifiableSet(map.entrySet());
    }

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

    @Override
    public Set<Key> keySet() {
        return Collections.unmodifiableSet(map.keySet());
    }

    @Override
    public void putAll(Map<? extends Key, ? extends Value> m) {
        m.entrySet().forEach(e -> put(e.getKey(), e.getValue()));
    }

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

    @Override
    public Collection<Value> values() {
        return Collections.unmodifiableCollection(map.values());
    }

    @Override
    public Value get(Object key) {
        return map.get(key);
    }

    @Override
    public Value put(Key key, Value value) {
        Value v = remove(key);
        getReverseView().remove(value);
        map.put(key, value);
        revMap.put(value, key);
        return v;
    }

    public Map<Value, Key> getReverseView() {
        return new BidirectionalMap<>(revMap, map);
    }

    @Override
    public Value remove(Object key) {
        if (containsKey(key)) {
            Value v = map.remove(key);
            revMap.remove(v);
            return v;
        } else {
            return null;
        }
    }

}
Community
  • 1
  • 1
Qw3ry
  • 1,319
  • 15
  • 31
  • note that just as BiMap and BidiMap, this is a bijection which does not allow to have multiple keys with same value. (getReverseView().get(v) will always return only one key). – Donatello Apr 22 '20 at 19:48
  • True, but OTOH thats exactly what the OP asked for – Qw3ry Apr 22 '20 at 20:20
  • I am not sure he expressed that its data match this constraint, but anyway, it could help someone else to better understand ! – Donatello Apr 22 '20 at 20:41
0

Quite an old question here, but if someone else has brain block like I just did and stumbles on this, hopefully this will help.

I too was looking for a bi-directional HashMap, sometimes it is the simplest of answers that are the most useful.

If you do not wish to re-invent the wheel and prefer not to add other libraries or projects to your project, how about a simple implementation of parallel arrays (or ArrayLists if your design demands it).

SomeType[] keys1 = new SomeType[NUM_PAIRS];
OtherType[] keys2 = new OtherType[NUM_PAIRS];

As soon as you know the index of 1 of the two keys you can easily request the other. So your lookup methods could looks something like:

SomeType getKey1(OtherType ot);
SomeType getKey1ByIndex(int key2Idx);
OtherType getKey2(SomeType st); 
OtherType getKey2ByIndex(int key2Idx);

This is assuming you are using proper object oriented structures, where only methods are modifying these arrays/ArrayLists, it would be very simple to keep them parallel. Even easier for an ArrayList since you would not have to rebuild if the size of the arrays change, so long as you add/remove in tandem.

NekoKikoushi
  • 496
  • 5
  • 16
  • 3
    You're losing an important feature of HashMaps, namely, O(1) lookup. An implementation like this would require scanning through one of the arrays until you find the index of the item you are looking for, which is O(n) – Kip Feb 01 '15 at 20:29
  • Yeah that's very true and is one rather large drawback. However in my personal situation I was actually dealing with a need for a tri-directional key listing and I always would know at least 1 of the keys ahead of time, so for me personally that wasn't an issue. Thank you for pointing that out though, I seem to have skipped that important fact in my original post. – NekoKikoushi Feb 03 '15 at 15:48