3

Suppose I have a HashMap<K, V> and two objects of type K that are equal to each other but not the same object, and the map has an entry for key k1.

Given k2, can I get a reference to k1 using only methods from HashMap (no external data structures) that executes in constant time, ie O(1) time complexity?

In code:

K k1, k2;
k1.equals(k2) // true
k1.hashCode() == k2.hashCode() // true
k1 == k2 // false
myMap.put(k1, someValue);

K existingKey = getExistingKey(myMap, k2);
existingKey == k1 // true <- this is the goal

<K> K getExistingKey(HashMap<K, V> map, K k) {
    // What impl goes here?
}

I was hoping to use one of the various methods added with java 8, such as compute() to "sniff" the existing key within the lambda, but they all (seem to) pass the new key object to the lambda, not the existing key.

Iterating through the entrySet() would find the existing key, but not in constant time.

I could use a Map<K, K> to store the keys and I could keep it in sync, but that doesn't answer the question.

Bohemian
  • 412,405
  • 93
  • 575
  • 722
  • Is this even possible? `k1.equals(k2) // true k1.hashCode() == k2.hashCode() // true k1 == k2 // false` – Cardinal System Dec 18 '17 at 04:57
  • 2
    Of course it's possible, and this is the case for the vast majority of object comparisons. `k1==k2` only if it's the same object instance. – Jim Garrison Dec 18 '17 at 04:58
  • It might help if you explained the use case where this would be important. Is there other state in `k2` not forming part of `equals()/hashCode()` that you need to access? That sort of breaks the implicit contract for those methods. – Jim Garrison Dec 18 '17 at 04:59
  • @CardinalSystem example: `List a = Arrays.asList("foo"); List b = Arrays.asList("foo");` would obey those conditions. – Bohemian Dec 18 '17 at 05:01
  • @Jim I'm implementing a DAWG and the keys are subtrees in a graph. I need the reference to the existing tree to reuse the same exact object. – Bohemian Dec 18 '17 at 05:03
  • It hardly has any meaning to restrict the solution to have no explicit external data structures, since if you can get a peer keys, thus there is a data structure is held alongside that contains these keys. I would suggest you to implement your variation of a `Map` interface that supports such retrieval facility by means of maintaining a separate collection of peer keys. – Pavel Dec 18 '17 at 05:18
  • I don't think you have a choice, either call `getNode` via reflection (which is a pain and could break later - it *was* named `getEntry` before...) or use `entrySet` – Eugene Dec 18 '17 at 10:00
  • @Pavel there will be millions of nodes - it's a memory usage issue. – Bohemian Dec 18 '17 at 12:27
  • @Eugene AFAIK there has never been a public method called `getEntry()` on any `Map` in java. – Bohemian Dec 18 '17 at 12:28
  • I've done a search to find the exact version where it was renamed, and stumbled across https://stackoverflow.com/questions/1873330/why-is-getentryobject-key-not-exposed-on-hashmap – Eugene Dec 18 '17 at 12:31
  • I never said it was public btw – Eugene Dec 18 '17 at 12:43
  • @FedericoPeraltaSchaffner and how, *exactly*, would I do that without an external data structure? I have many thousands of keys, possibly millions. I want to save memory. As per my question, I could put all the keys in a map of key -> key (like what backs a set), but that solution is out of scope. – Bohemian Dec 18 '17 at 14:31
  • @Bohemian, it is not possible to save memory restricting the solution. As I said, if you'd like to get all peer keys to the one you've provided, it should be stored in memory anyway. Thus, a custom implementation of a `Map` interface that stores peer keys explicitly would not take more space in memory. – Pavel Dec 19 '17 at 03:01

2 Answers2

1

You are looking for something like

Map.Entry<K,V> getEntry(K key)

At first I thought it would be easy to make a custom subclass of HashMap to return this, as get(K key) is just

public V get(Object key) {
     Node<K,V> e;
     return (e = getNode(hash(key), key)) == null ? null : e.value;
}

where Node implements Map.Entry. This would look like:

public class MyHashMap<K,V> extends HashMap<K,V>
{
    public MyHashMap() {}
    // Other constructors as needed

    public Map.Entry<K, V> getEntry(K key)
    {
        Map.Entry<K, V> e = getNode(hash(key),key);
        return e;
    }
}

Unfortunately, getNode() and hash() are both package private and therefore not visible to subclasses.

The next step was to put the class in java.util but this fails in Java 9 with

The package java.util conflicts with a package accessible from another module: java.base

I think you're out of luck here.

I actually think a getEntry() method would be a useful addition to the API, you might consider filing an enhancement request.

Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • To circumvent package private, you could create such a class in the package `java.util` in your own project. Why not enhance your answer with that in mind. Also, make the method `getKey()` - getting the value is trivial. – Bohemian Dec 18 '17 at 05:35
  • Already tried that. This fails in Java 9 with modules, the error is `The package java.util conflicts with a package accessible from another module: java.base`. If there's a way around that I haven't been able to find it. – Jim Garrison Dec 18 '17 at 06:04
0

I don't know how constrained you are regarding memory usage, but if you can use a LinkedHashMap instead of a HashMap (LinkedHashMap uses extra references to keep insertion order), then you could take advantage of its removeEldestEntry method:

public class HackedMap<K, V> extends LinkedHashMap<K, V> {

    K lastKey;

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        lastKey = eldest.getKey();
        return false;
    }

    K getLastKey() {
        return lastKey;
    }
}

I think the code is self-explanatory. We are keeping a reference to the original key, which we grab from the removeEldestEntry method's argument. As to the removeEldestEntry method's return value, it is false, so that we don't allow the eldest entry to be removed (after all, we don't want the map to work as a cache).

Now, with a common insertion-order LinkedHashMap, the removeEldestEntry method is automatically called by put and putAll (from removeEldestEntry method docs):

This method is invoked by put and putAll after inserting a new entry into the map.

So all we need to do now is to implement your getExistingKey method in such a way that it calls put without modifying the map, which you can do as follows:

<K, V> K getExistingKey(HackedMap<K, V> map, K k) {
    if (k == null) return null;
    V v = map.get(k);
    if (v == null) return null;
    map.put(k, v);
    return map.getLastKey();
}

This works because, when the map already contains an entry mapped to a given key, the put method replaces the value without touching the key.

I'm not sure about the null checks I've done, maybe you need to improve that. And of course this HackedMap doesn't support concurrent access, but HashMap and LinkedHashMap don't do either.

You can safely use a HackedMap instead of a HashMap. This is the test code:

Key k1 = new Key(10, "KEY 1");
Key k2 = new Key(10, "KEY 2");
Key k3 = new Key(10, "KEY 3");

HackedMap<Key, String> myMap = new HackedMap<>();

System.out.println(k1.equals(k2)); // true
System.out.println(k1.equals(k3)); // true
System.out.println(k2.equals(k3)); // true

System.out.println(k1.hashCode() == k2.hashCode()); // true
System.out.println(k1.hashCode() == k3.hashCode()); // true
System.out.println(k2.hashCode() == k3.hashCode()); // true

System.out.println(k1 == k2); // false
System.out.println(k1 == k3); // false
System.out.println(k2 == k3); // false

myMap.put(k1, "k1 value");
System.out.println(myMap); // {Key{k=10, d='KEY 1'}=k1 value}

myMap.put(k3, "k3 value"); // Key k1 (the one with its field d='KEY 1') remains in
                           // the map but value is now 'k3 value' instead of 'k1 value'

System.out.println(myMap); // {Key{k=10, d='KEY 1'}=k3 value}

Key existingKey = getExistingKey(myMap, k2);

System.out.println(existingKey == k1); // true
System.out.println(existingKey == k2); // false
System.out.println(existingKey == k3); // false

// Just to be sure
System.out.println(myMap); // {Key{k=10, d='KEY 1'}=k3 value}

Here's the Key class I've used:

public class Key {

    private final int k;

    private final String d;

    Key(int k, String d) {
        this.k = k;
        this.d = d;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Key key1 = (Key) o;
        return k == key1.k;
    }

    @Override
    public int hashCode() {
        return Objects.hash(k);
    }

    @Override
    public String toString() {
        return "Key{" +
                "k=" + k +
                ", d='" + d + '\'' +
                '}';
    }
}
fps
  • 33,623
  • 8
  • 55
  • 110
  • I thought about this, in the sense of using `TreeMap#ceilingEntry` – Eugene Dec 18 '17 at 20:03
  • Your suggestion doesn't "satisfy all my requirements"; it puts the new key object into the map, but I never want to overwrite an existing key. Instead, I want to put a new value without affecting the key. That way, the map functions as a cache with metadata/attributes. If I encounter a key collision, I want to throw away my new key and use the key object already in the map. – Bohemian Dec 18 '17 at 20:03
  • @Bohemian right. I would honestly go the reflective call of the the method, also providing a unit test as a safety net in case it gets changed. – Eugene Dec 18 '17 at 20:08
  • @Bohemian You are wrong. The key is never overwritten when there's already an entry mapped to an equal key. Let me modify the testing code and you'll see... – fps Dec 18 '17 at 20:20
  • @FedericoPeraltaSchaffner I was not referring to the overriding part - I *know* it is not overridden. – Eugene Dec 18 '17 at 20:45
  • @Eugene Agreed: reflection plus unit tests, especially to guard against changes to the way hash codes are actually used, is where I'm going – Bohemian Dec 18 '17 at 20:54
  • @FedericoPeraltaSchaffner its too late for me to think.. will take a closer look tomorrow – Eugene Dec 19 '17 at 03:13