0

Is there any solution which allows to avoid usage of two maps for case when two keys are mapped to the same value? The problem: we have a server that receives two types of requests - getDataByUserID(UserID userId) and getDataByNodeID(NodeID nodeId), where userId and nodeId have one-to-one mapping. No DB used, all data is stored in memory. There is straightforward solution - to use two maps one UserID/data and second NodeID/data, but I would like to avoid manipulations with two tables. The server interface is:

void put (InetSocketAddress nodeId, String clientId, Data data);
Data get (InetSocketAddress nodeId);
Data get (String clientId);

Any suggestions are welcome.

bw_dev
  • 775
  • 1
  • 7
  • 17
  • Just check `if ( map.containsValue( obj ))`! – Omid.N Nov 07 '20 at 12:49
  • I don't understand the problem. When two keys point to the same value, why do you need to use two maps then? Then you have two keys in your map .. that map to the same value. – akuzminykh Nov 07 '20 at 12:49
  • @ akuzminykh Map allows only one key. In my case for each request I know either userId or nodeId and never both. – bw_dev Nov 07 '20 at 12:52
  • But you have two different types there? – akuzminykh Nov 07 '20 at 12:53
  • Does this answer your question? [HashMap with multiple values under the same key](https://stackoverflow.com/questions/4956844/hashmap-with-multiple-values-under-the-same-key) – JavaMan Nov 07 '20 at 12:54
  • @akuzminykh Currently - no, by I may change type of one of IDs – bw_dev Nov 07 '20 at 12:55
  • @JavaMan No it is the case when several values are mapped to the singe key – bw_dev Nov 07 '20 at 12:57

3 Answers3

1

Maybe adding an interface to UserId and NodeId could work.

Something like this:

void test()
{
    UserId userId = new UserId();
    NodeId nodeId = new NodeId();
    String userData = "xyz";

    HashMap<IdValue, String> idToDataMap = new HashMap<>();
    idToDataMap.put(userId,userData);
    idToDataMap.put(nodeId,userData);
}

class UserId implements IdValue {
    //...
}

class NodeId implements IdValue {
    //...
}

interface IdValue {
}
Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
JavaMan
  • 1,142
  • 12
  • 22
  • Great! It looks like solution. The only thing I need to do is to change keys to be the same java type. THANKS! – bw_dev Nov 07 '20 at 13:10
  • You shouldn't say "thanks for a solution" without confirming that it is a solution. This won't work. Or at least, not without other changes as well. – Stephen C Nov 07 '20 at 13:11
  • @Stephen C Sorry, but why it "won't work"? In case of two maps I store reference to data twice. In case proposed above the same, but I don't need to keep two maps synchronized. The only thing I have to do is to "normalize" type of keys. – bw_dev Nov 07 '20 at 13:22
  • OK. I see what you did. – Stephen C Nov 07 '20 at 13:33
  • The only issues are that you have duplicate entries, and that you probably need to do more work to avoid race conditions if the map is used by multiple threads. (Using a concurrent map won't solve that.) – Stephen C Nov 07 '20 at 13:36
  • @StephenC Yes, and even more - more time for rehashing. I don't know why, but my tests show that this time grows exponentially. So, rehash of two short tables takes less time that one big table :) – bw_dev Nov 07 '20 at 13:38
  • Even so, this is as good as you are going to get. – Stephen C Nov 07 '20 at 13:39
1

There is no smart data structure to do this. As @JavaMan suggests, you could use one HashMap with keys of both types if the keys have a common supertype. However, this will be difficult to implement without race conditions if you need a concurrent solution. (You cannot add or remove two entries atomically ...)

There is a nasty solution that avoids this. You could redefine the NodeId and UserId classes to implement a common interface; say CommonId. Then you need to redefine the equals(Object) and hashCode() methods so that they treat the two kinds of identifier as equivalent.

For example:

UserId a = ...
NodeId b = ... // representing the same user as 'a'

Then

a.equals(b) => true
b.equals(a) => true
a.hashCode() == b.hashCode()

in addition to the other aspects of the equals / hashcode contract.

Important Note: this assumes that there is an efficient way to implement the above semantics that doesn't rely on the map that we are defining.

You can then change your map to a HashMap<CommonId, YourValueClass>, and put and get using either a UserId instance or NodeId instance as the key.

Why is this nasty?

  • Because this redefinition of equals and hashCode applies for all uses of the two classes, not just this map.

  • Because it is violating the documented semantics of equals(Object). The javadocs say that this.equals(other) should return false if this and other have different classes.

However, this does suggest some alternative solutions:

  1. You could use a TreeMap instead of a HashMap, and supply a Comparator<CommonId> that provides a consistent ordering, AND treats equivalent identifiers of either type as equal.

    • It is not clear whether it would be feasible to implement the ordering.
    • TreeMap is O(logN) rather than O(1) for get and put operations.
  2. You could try to find a 3rd-party hash map implementation that allows you to supply hash and equals functions; i.e. analogous to supplying a Comparator to a TreeMap.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
0

Sure. Plenty of solutions.

Make your own class

Make a class. This class internally has 2 fields: Map<UserId, Data> userIdToData and Map<NodeId, Data> nodeIdToData. These are private fields. The class does not itself implement java.util.Map, but it has many of its methods, such as size() (which just does return userIdToData.size();). It also has:

public void put(UserId userId, NodeId nodeId, Data data) {
    userIdToData.put(userId, data);
    nodeIdToData.put(nodeId, data);
}

public Data getByUserId(UserId userId) {
    return userIdToData.get(userId);
}

// ... and getByNodeId, and getByUserIdOrDefault, etcetera.

THIS code will be messing with 2 maps and needs to ensure they remain in sync, but you can trivially write extensive tests for this code and all other code in your entire project doesn't need to worry about it then.

map UserId to NodeId, then NodeId to data

Have 2 maps:

Map<UserId, NodeId> userToNode;
Map<NodeId, Data> nodeToData;

and a method:

public Data getByUserId(UserId id) {
    NodeId node = userToNode.get(id);
    if (node == null) return null;
    return nodeToData.get(node);
}
rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Sorry, but it is my current implementation (wrapper). My question is it possible to avoid two maps, by any other "smart" data structure – bw_dev Nov 07 '20 at 13:07
  • Is it _possible_? Yes, but, what would it look like? A single HashMap, mapping an Object to a Data, where the keys are a mix of NodeIds and UserIds? That is _possible_ but then each Data shows up as value twice, you'd have to divide size() by 2, etc, etc - an inferior solution in all relevant ways which is why I didn't mention it. You're fishing for an implementation to a solution you cooked up in your head. That's not how to do it. You are looking for a solution to your problem. Your problem is not 'how do I mix keys in a single map'. Your problem is 'how do I efficiently look up by either'. – rzwitserloot Nov 07 '20 at 13:41