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.
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.
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 + '\'' +
'}';
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.