Multiple key on HashMap: it deletes existing values? - java

I have implemented my multiple key class as follows:
public class ProbabilityIndex {
private int trueLabel;
private int classifiedLabel;
private int classifierIndex;
public ProbabilityIndex(int trueLabel, int classifiedLabel, int classifierIndex) {
this.trueLabel = trueLabel;
this.classifiedLabel = classifiedLabel;
this.classifierIndex = classifierIndex;
}
#Override
public boolean equals(Object obj) {
if ( !obj instanceof ProbabilityIndex)
return false;
if (obj == this)
return true;
ProbabilityIndex rhs = (ProbabilityIndex) obj;
return new EqualsBuilder().
append(trueLabel, rhs.trueLabel).
append(classifiedLabel, rhs.classifiedLabel).
append(classifierIndex, rhs.classifierIndex).
isEquals();
}
#Override
public int hashCode() {
int hashCode = new HashCodeBuilder(17, 31).
append(trueLabel).
append(classifiedLabel).
append(classifierIndex).
toHashCode();
return hashCode;
}
}
Notice that trueLabel, classifiedLabel and classifierIndex are all either 0 or 1.
Then, I use my key as follows:
ProbabilityIndex key = new ProbabilityIndex(trueLabel, classifiedLabel, classifierIndex);
probabilities.put(key, new Double(value));
where probabilities is declared as follows:
HashMap<ProbabilityIndex, Double> probabilities;
However, different combinations of trueLabel, classifiedLabel and classifierIndex write the tuple in the same position in probabilities, thus overwriting existing tuples.
How can I overcome this issue?
Minimal test case:
HashMap<ProbabilityIndex, Double> map = new HashMap<ProbabilityIndex, Double>();
map.put(new ProbabilityIndex(0, 0, 0), new Double(0.1));
map.put(new ProbabilityIndex(0, 0, 1), new Double(0.2));
map.put(new ProbabilityIndex(0, 1, 0), new Double(0.1));
map.put(new ProbabilityIndex(0, 1, 1), new Double(0.2));
map.put(new ProbabilityIndex(1, 0, 0), new Double(0.1));
This inserts 4 tuples instead of 5.

I can only tell you that the hashtable will never overwrite objects with the same hash code (a hash collision); it will just be less efficient in their retrieval.
The only way to have your entries incorrectly overwritten is by providing an equals method for the key which returns true for different keys.
A bit of further advice not directly related to your problem: if all you have is three two-state variables, then the complete value set for the class has cardinality of just 8. Instead of the complicated hash code builder you use, you could just construct the hash code with three bits, each representing one variable. That would plainly ensure that each state of your object has a distinct hash code.
I have verified your code with the following implementations of hashCode() and equals() (I had to change equals to make your example truly self-contained):
#Override public boolean equals(Object obj) {
if (!(obj instanceof ProbabilityIndex)) return false;
if (obj == this) return true;
ProbabilityIndex rhs = (ProbabilityIndex) obj;
return this.trueLabel == rhs.trueLabel
&& this.classifiedLabel == rhs.classifiedLabel
&& this.classifierIndex == rhs.classifierIndex;
}
#Override public int hashCode() {
return trueLabel | (classifiedLabel << 1) | (classifierIndex << 2);
}
Your test code resulted in a map with five entries.
As a final note, you don't even need a hashtable if its maximum size will be only 8. A plain array of size 8, indexed by the above hash code, would be enough.

Related

How to use an Object array as a key in hashmap

I have an object array called Position[] that returns an array of Position objects. I would like to use this as my key for a hashmap for the following: HashMap<Position[],Double> h = new HashMap<>();
I understand that arrays have different hashcodes even if the elements are the same. So I went ahead and tried to override the equals and hashcode. This was my attempt:
public class Key {
private Position p1;
private Position p2;
public Key(Position p1, Position p2){
this.p1 = p1;
this.p2 = p2
}
#Override
public boolean equals(Object object) {
if (!(object instanceof Key)) {
return false;
}
Key newKey = (Key) object;
return this.hashCode()== newKey.hashCode(); //bit of a hack way
}
#Override
public int hashCode(){
int result = 17;
result = 31 * result + this.p1.hashCode();
result = 31 * result + this.p2.hashCode();
return result;
}
}
So I had to change my map to HashMap<Key,Double> However, when ever i go to get the value using the key is still returns null.
An example of what can be passed into they constructor of Key are G2 G4 or E4 E6 ETC.
How would I go about achieving this so that the comparisons actually work?
Thanks.
You can create a special wrapper object to use Position[] as a key in a Map, using Arrays.deepEquals(Object[], Object[]) and Arrays.deepHashCode(Object[]) in the implementations of equals(Object) and hashCode().
import java.util.Arrays;
public final class PositionArrayKey {
private final Position[] array;
public PositionArrayKey(Position[] array) {
this.array = array;
}
#Override
public boolean equals(Object object) {
if (object == this) return true;
if (!(object instanceof PositionArrayKey)) return false;
return Arrays.deepEquals(this.array, ((PositionArrayKey) object).array);
}
#Override
public int hashCode() {
return Arrays.deepHashCode(this.array);
}
}
This enables storing Position[] array instances as keys in map, when wrapped. E.g.
Map<PositionArrayKey, Object> map = new HashMap<>();
map.put(new PositionArrayKey(new Position[]{...}), ...);
Object value = map.get(new PositionArrayKey(new Position[]{...}), ...);
(assuming that both of the Position[] arrays are deeply equal in this example)
Note that for large arrays, performance for invoking equals(Object) and hashCode() may be slow. You can modify the above snippet to cache the result Arrays.deepHashCode(this.array) for larger arrays, if you find it necessary.

Java TreeMap put vs HashMap put, custom Object as key

My objective was to use the TreeMap to make Box key objects sorted by Box.volume attribute while able to put keys distinct by the Box.code. Is it not possible in TreeMap?
As per below test 1, HashMap put works as expected, HashMap keeps both A, B key objects, but in test 2, TreeMap put doesn't treat D as a distinct key, it replaces C's value, note that i used the TreeMap comparator as Box.volume, because i want keys to be sorted by volume in TreeMap.
import java.util.*;
public class MapExample {
public static void main(String[] args) {
//test 1
Box b1 = new Box("A");
Box b2 = new Box("B");
Map<Box, String> hashMap = new HashMap<>();
hashMap.put(b1, "test1");
hashMap.put(b2, "test2");
hashMap.entrySet().stream().forEach(o-> System.out.println(o.getKey().code+":"+o.getValue()));
//output
A:test1
B:test2
//test 2
Box b3 = new Box("C");
Box b4 = new Box("D");
Map<Box, String> treeMap = new TreeMap<>((a,b)-> Integer.compare(a.volume, b.volume));
treeMap.put(b3, "test3");
treeMap.put(b4, "test4");
treeMap.entrySet().stream().forEach(o-> System.out.println(o.getKey().code+":"+o.getValue()));
//output
C:test4
}
}
class Box {
String code;
int volume;
public Box(String code) {
this.code = code;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Box box = (Box) o;
return code.equals(box.code);
}
#Override
public int hashCode() {
return Objects.hash(code);
}
}
Thank you
TreeMap considers 2 keys for which the comparison method returns 0 to be identical, even if they are not equal to each other, so your current TreeMap cannot contain two keys with the same volume.
If you want to keep the ordering by volume and still have multiple keys with the same volume in your Map, change your Comparator's comparison method to compare the Box's codes when the volumes are equal. This way it will only return 0 if the keys are equal.
Map<Box, String> treeMap = new TreeMap<>((a,b)-> a.volume != b.volume ? Integer.compare(a.volume, b.volume) : a.code.compareTo(b.code));
Now the output is:
C:test3
D:test4
b3 and b4 has the same volume, that is 0 (int default value).
For it work, assign a value to the Box volume variables before comparing.

Weird equals() result with Map/Set object graph

Investigating a special case where some objects didn't equal as they should and came to this simple test case that simplifies my issue.
When running this with JUnit in Eclipse with jdk8u152 the last assertEquals fails, can anyone explain why?
It's something with Set/HashSet because if I change as,bs to be ArrayList's instead the final assertEquals goes through.
#Test
public void test()
{
String list = "list";
String object = "object";
String value = "value";
Map<String, Object> a = new HashMap<>();
Map<String, Object> b = new HashMap<>();
assertEquals(a, b);
Set<Object> as = new HashSet<>();
Set<Object> bs = new HashSet<>();
a.put(list, as);
b.put(list, bs);
assertEquals(a, b);
Map<String, Object> ao = new HashMap<>();
as.add(ao);
Map<String, Object> bo = new HashMap<>();
bs.add(bo);
assertEquals(a, b);
ao.put(object, value);
bo.put(object, value);
assertEquals(a, b);
}
You're mutating the elements of the sets. That leads to unspecified behaviour.
From the JavaDoc:
Great care must be exercised if mutable objects are used as set elements. The behavior of a set is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is an element in the set.
You are adding ao and bo HashMaps to the HashSets as and bs.
Later you mutate ao and bo by putting a new entry in each of them.
This means that the hashCode that was used to place ao in as is no longer the current hashCode of ao, and the hashCode that was used to place bo in bs is no longer the current hashCode of bo.
As a result, AbstractSet's equals cannot locate the element of one Set in the other Set, so it concludes that as is not equal to bs. As a result a is not equal to b.
Here's the implementation of AbstractSet's equals. You can see that it uses containsAll, which in turns calls contains(), which relies on the hashCode of the searched element. Since that hashCode has changed after the element was added to the Set, contains() doesn't find the element.
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
If you mutate an element of a HashSet in a way that affects the result of equals or hashCode, you must remove the element from the HashSet prior to the update and add it again after the update.
Adding the following remove and add calls will cause a to be equal to b in the end:
....
assertEquals(a, b);
bs.remove (bo); // added
as.remove (ao); // added
ao.put(object, value);
bo.put(object, value);
as.add (ao); // added
bs.add (bo); // added
assertEquals(a, b);
That is because of the hascode implementation of HashMap which is basically x-or of key and value. If key or value is null then hascode will be zero. Hence all empty hashmaps will have hashcode as zero.
/*hashcode of HashMap*/
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
/*hashcode of object*/
public static int hashCode(Object o) {
return o != null ? o.hashCode() : 0;
}
Upon adding key value pairs the hashcode value changes.

How to override equals for two Maps of <String, Object>?

I have a class that has a Map<String, Object> field (the keys are Strings, the values are Objects that have correctly implemented the "equals" method for comparison).
I would like to override equals for this class in a way that only returns true if the Maps have equal mappings between keys and values.
Here is my attempt:
// Assumes that the Object values in maps have correctly implemented the equals method.
private boolean mapsEqual(Map<String, Object> attributes)
{
if (this.attributes_.keySet().size() != attributes.keySet().size() ||
this.attributes_.values().size() != attributes.values().size())
return false;
for (String key : attributes.keySet()) {
if (!this.attributes_.keySet().contains(key))
return false;
if (!this.attributes_.get(key).equals(attributes.get(key)))
return false;
}
return true;
}
However, this implementation fails when the same key is added more than once or when a key is removed from the map (the size tests fail for the values, as they count the duplicates and do not resize when values are removed.)
It seems that my situation should be common enough to find information that is relevant to my case, but I could not find any. Is there any legacy code or widely accepted solution to this situation? Any help or working solution is appreciated.
I am going to put this as an answer even though I am not 100% sure it solves your problem (but it's simply not gonna fit in a comment).
First off, to repeat my comments: The Map interface forbides that a map has duplicate keys or multiple values per key. Any proper implementation (e.g. java.util.HashMap) will therefore not allow this. Typically they will just replace the value if this happens.
Furthermore, the specification for equals, to me, seems to be doing what you want. Again, a proper implementation must live up to that specification.
So, what's the point here: If you are writing your own class that is implementing Map, then it simply cannot allow duplicate keys (methods like get wouldn't make sense anymore). If you are using a built-in implementation such as HashMap, it replaces the values anyway.
Now you are saying that you're experiencing size issues with keySet() and values(). I think you should add example code that will cause this behavior. The following works just fine for me:
Map<String, String> map = new HashMap<String, String>();
map.put("Foo", "Bar");
System.out.println(map.keySet().size()); // 1
System.out.println(map.values().size()); // 1
map.put("Foo", "Baz"); // the HashMap will merely replace the old value
System.out.println(map.keySet().size()); // still 1
System.out.println(map.values().size()); // still 1
Removing a key will, of course, change the size. I don't see how you consider this a problem based on your explanations so far.
As for equals, you may just want to look at the implementation for HashMap, which can be found here:
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<K,V> m = (Map<K,V>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
Consider the following example:
Map<String, String> map1 = new HashMap<String, String>();
map1.put("Foo", "Bar");
Map<String, String> map2 = new HashMap<String, String>();
map2.put("Foo", "Bar");
System.out.println(map1.equals(map2)); // true
Firstly, you complain about your maps having duplicate keys... not possible (unless you're using a badly broken implementation).
This should do it:
public boolean equals(Object o) {
if (!(o instanceof MyClass))
return false;
MyClass that = (MyClass)o;
if (map.size() != that.map.size())
return false;
for (Map.Entry<String, Object> entry : map) {
Object a = entry.getValue();
Object b = that.map.get(entry.getKey());
if ((a == null ^ b == null) || (a == null && !a.equals(b)))
return false;
}
return true;
}

ArrayList as key in HashMap

Would it be possible to add an ArrayList as the key of HashMap. I would like to keep the frequency count of bigrams. The bigram is the key and the value is its frequency.
For each of the bigrams like "he is", I create an ArrayList for it and insert it into the HashMap. But I am not getting the correct output.
public HashMap<ArrayList<String>, Integer> getBigramMap(String word1, String word2) {
HashMap<ArrayList<String>, Integer> hm = new HashMap<ArrayList<String>, Integer>();
ArrayList<String> arrList1 = new ArrayList<String>();
arrList1 = getBigram(word1, word2);
if (hm.get(arrList1) != null) {
hm.put(arrList1, hm.get(arrList1) + 1);
} else {
hm.put(arrList1, 1);
}
System.out.println(hm.get(arrList1));
return hm;
}
public ArrayList<String> getBigram(String word1, String word2) {
ArrayList<String> arrList2 = new ArrayList<String>();
arrList2.add(word1);
arrList2.add(word2);
return arrList2;
}
Yes you can have ArrayLists as a keys in a hash map, but it is a very bad idea since they are mutable.
If you change the ArrayList in any way (or any of its elements), the mapping will basically be lost, since the key won't have the same hashCode as it had when it was inserted.
The rule of thumb is to use only immutable data types as keys in a hash map. As suggested by Alex Stybaev, you probably want to create a Bigram class like this:
final class Bigram {
private final String word1, word2;
public Bigram(String word1, String word2) {
this.word1 = word1;
this.word2 = word2;
}
public String getWord1() {
return word1;
}
public String getWord2() {
return word2;
}
#Override
public int hashCode() {
return word1.hashCode() ^ word2.hashCode();
}
#Override
public boolean equals(Object obj) {
return (obj instanceof Bigram) && ((Bigram) obj).word1.equals(word1)
&& ((Bigram) obj).word2.equals(word2);
}
}
Why can't you use something like this:
class Bigram{
private String firstItem;
private String secondItem;
<getters/setters>
#Override
public int hashCode(){
...
}
#Override
public boolean equals(){
...
}
}
instead of using the dynamic collection for limited number of items (two).
From the documentation:
Note: great care must be exercised if mutable objects are used as map
keys. The behavior of a map is not specified if the value of an object is
changed in a manner that affects equals comparisons while the
object is a key in the map. A special case of this prohibition is that it
is not permissible for a map to contain itself as a key. While it is
permissible for a map to contain itself as a value, extreme caution is
advised: the equals and hashCode methods are no longer
well defined on such a map.
You have to take care when you are using mutable objects as keys for the sake of hashCode and equals.
The bottom line is that it is better to use immutable objects as keys.
Try this ,this will work.
public Map<List, Integer> getBigramMap (String word1,String word2){
Map<List,Integer> hm = new HashMap<List, Integer>();
List<String> arrList1 = new ArrayList<String>();
arrList1 = getBigram(word1, word2);
if(hm.get(arrList1) !=null){
hm.put(arrList1, hm.get(arrList1)+1);
}
else {
hm.put(arrList1, 1);
}
System.out.println(hm.get(arrList1));
return hm;
}
I've come up with this solution. It is obviously not usable in all cases, for example over stepping the hashcodes int capacity, or list.clone() complications(if the input list gets changed, key stays the same as intended, but when the items of List are mutable, cloned list has the same reference to its items, which would result in changing the key itself).
import java.util.ArrayList;
public class ListKey<T> {
private ArrayList<T> list;
public ListKey(ArrayList<T> list) {
this.list = (ArrayList<T>) list.clone();
}
#Override
public int hashCode() {
final int prime = 31;
int result = 1;
for (int i = 0; i < this.list.size(); i++) {
T item = this.list.get(i);
result = prime * result + ((item == null) ? 0 : item.hashCode());
}
return result;
}
#Override
public boolean equals(Object obj) {
return this.list.equals(obj);
}
}
---------
public static void main(String[] args) {
ArrayList<Float> createFloatList = createFloatList();
ArrayList<Float> createFloatList2 = createFloatList();
Hashtable<ListKey<Float>, String> table = new Hashtable<>();
table.put(new ListKey(createFloatList2), "IT WORKS!");
System.out.println(table.get(createFloatList2));
createFloatList2.add(1f);
System.out.println(table.get(createFloatList2));
createFloatList2.remove(3);
System.out.println(table.get(createFloatList2));
}
public static ArrayList<Float> createFloatList() {
ArrayList<Float> floatee = new ArrayList<>();
floatee.add(34.234f);
floatee.add(new Float(33));
floatee.add(null);
return floatee;
}
Output:
IT WORKS!
null
IT WORKS!
Sure it possible. I suppose the issue in your put. Try obtain key for bigram, increment it, remove entry with this bigram and insert updated value
Unlike Array, List can be used as the key of a HashMap, but it is not a good idea, since we should always try to use an immutable object as the key.
.toString() method getting the String represtenation is a good key choice in many cases, since String is an immuteable object and can prefectly stands for the array or list.
Please check below my code in order to understand if key is ArrayList in Map and how JVM will do it for inputs:
here i write hashCode and equals method for TesthashCodeEquals class.
package com.msq;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class TesthashCodeEquals {
private int a;
private int b;
public TesthashCodeEquals() {
// TODO Auto-generated constructor stub
}
public TesthashCodeEquals(int a, int b) {
super();
this.a = a;
this.b = b;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public int getB() {
return b;
}
public void setB(int b) {
this.b = b;
}
public int hashCode() {
return this.a + this.b;
}
public boolean equals(Object o) {
if (o instanceof TesthashCodeEquals && o != null) {
TesthashCodeEquals c = (TesthashCodeEquals) o;
return ((this.a == c.a) && (this.b == c.b));
} else
return false;
}
}
public class HasCodeEquals {
public static void main(String[] args) {
Map<List<TesthashCodeEquals>, String> m = new HashMap<>();
List<TesthashCodeEquals> list1=new ArrayList<>();
list1.add(new TesthashCodeEquals(1, 2));
list1.add(new TesthashCodeEquals(3, 4));
List<TesthashCodeEquals> list2=new ArrayList<>();
list2.add(new TesthashCodeEquals(10, 20));
list2.add(new TesthashCodeEquals(30, 40));
List<TesthashCodeEquals> list3=new ArrayList<>();
list3.add(new TesthashCodeEquals(1, 2));
list3.add(new TesthashCodeEquals(3, 4));
m.put(list1, "List1");
m.put(list2, "List2");
m.put(list3, "List3");
for(Map.Entry<List<TesthashCodeEquals>,String> entry:m.entrySet()){
for(TesthashCodeEquals t:entry.getKey()){
System.out.print("value of a: "+t.getA()+", value of b: "+t.getB()+", map value is:"+entry.getValue() );
System.out.println();
}
System.out.println("######################");
}
}
}
.
output:
value of a: 10, value of b: 20, map value is:List2
value of a: 30, value of b: 40, map value is:List2
######################
value of a: 1, value of b: 2, map value is:List3
value of a: 3, value of b: 4, map value is:List3
######################
so this will check the number of objects in List and the values of valriabe in object. if number of objects are same and the values of instance variables is also same then it will consider duplicate key and override the key.
now if i change only the value of object on list3
list3.add(new TesthashCodeEquals(2, 2));
then it will print:
output
value of a: 2, value of b: 2, map value is:List3
value of a: 3, value of b: 4, map value is:List3
######################
value of a: 10, value of b: 20, map value is:List2
value of a: 30, value of b: 40, map value is:List2
######################
value of a: 1, value of b: 2, map value is:List1
value of a: 3, value of b: 4, map value is:List1
######################
so that It always check the number of objects in List and the value of instance variable of object.
thanks
ArrayList.equals() is inherited from java.lang.Object - therefore equals() on ArrayList is independent of the content of the list.
If you want to use an ArrayList as a map key, you will need to override equals() and hashcode() in order to make two arraylists with the same content in the same order return true on a call to equals() and return the same hashcode on a call to hashcode().
Is there any particular reason you have to use an ArrayList as opposed to say a simple String as the key?
edit: Ignore me, as Joachim Sauer pointed out below, I am so wrong it's not even funny.

Categories

Resources