Does HashMap's equals depend on EntrySet.keySet? - java

I have a Java Object, where I have overridden the equals() method for comparing all values. One of the member variables that I compare is a HashMap<String, MyObject>. Even if the keys and MyObjects stored in there are equal, the HashMaps seem not to be.
When I debug my tests and take a look into both HashMaps, I can see that the EntrySets are not equal. But the only difference I can see in them is that one has a EntrySet.keySet being null, and the other having the Entry.keySet being a KeySet object.
My question now is: does the equals() method of HashMap depend on this value or not?
(I don't want you to help me debug my code, I just want to understand what is happening)

You can check this easily by looking at the source of HashMap#Equals(). The values are compared for non-null keys.
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;
}
Edit:
Explanation: An Entry is a {key,value} pair. HashMap maintains these Entrys in an EntrySet. Now to compare two maps: I can just iterate through all the Entrys and and keep comparing Entry.value with anothermap.get(Entry.key). So, in essence the equals() of you value objects will matter when comparing maps.

Related

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;
}

Iterate through two TreeMaps to compare in Java

At first I had something like this:
public static boolean equals(TreeMap<?, Boolean> a, TreeMap<?, Boolean> b) {
boolean isEqual = false;
int count = 0;
if (a.size() == b.size()) {
for (boolean value1 : a.values()) {
for (boolean value2 : b.values()) {
if (value2 == value1) {
count++;
isEqual = true;
continue;
} else {
isEqual = false;
return isEqual;
}
}
}
if (count == a.size()) {
return true;
}
}
}
Then found that nope it didn't work. I'm checking to see if every element in Object a is the same as in Object b without using Iterate or Collection. and in the same place... any suggestions? Would implementing a for-each loop over the keySet() work?
So, something along these lines? Needing to take in account BOTH keys and values: (Not an answer - test code for suggestions)
This should work as values() are backed up by the TreeMap, so are sorted according to the key values.
List<Boolean> aList = new ArrayList<>(a.values());
List<Boolean> bList = new ArrayList<>(b.values());
boolean equal = aList.equals(bList);
This should be a bit faster than a HashSet version.
And this won't work as #AdrianPronk noticed:
a.values().equals(b.values())
How about this :
Set values1 = new HashSet(map1.values());
Set values2 = new HashSet(map2.values());
boolean equal = values1.equals(value2);
For Comparing two Map Objects in java, you can add the keys of a map to list and with those 2 lists you can use the methods retainAll() and removeAll() and add them to another common keys list and different keys list.
The correct way to compare maps is to:
Check that the maps are the same size(!)
Get the set of keys from one map
For each key from that set you retrieved, check that the value retrieved from each map for that key is the same

Java alternatives to PHP indexed arrays

I'm trying to iterate over an associative array and tally up how many instances of each combination there are (for use in determining conditional probability of A given B)
For example, in PHP I can iterate over the indexed array $Data[i] given input (A, ~B) and get a result of 2.
$Data[0] = array("A", "~B");
$Data[1] = array("~A", "B");
$Data[2] = array("A", "~B");
$Data[3] = array("A", "B");
I tried replicating this in Java with maps, but maps only allow a unique key for each value... So the following wouldn't work because key A is being used for three entries.
map.put("A", "~B");
map.put("~A", "B");
map.put("A", "~B");
map.put("A", "B");
Is there something else I can use?
Thanks!
You can use a Map<T,List<U>> (in your case it is Map<String,List<String>>) or you can use a Multimap<String,String> using some library such as guava (or apache commons version of it - MultiMap)
If iteration of the structure is your primary goal, a List<ConditionResult> would seem to be the most appropriate choice for your situation, where ConditionResult is given below.
If maintaining a count of the combinations is the sole goal, then a Map<ConditionResult,Integer> would also work well.
public class ConditionResult
{
// Assuming strings for the data types,
// but an enum might be more appropriate.
private String condition;
private String result;
public ConditionResult(String condition, String result)
{
this.condition = condition;
this.result = result;
}
public String getCondition() { return condition; }
public String getResult() { return result; }
public boolean equals(Object object)
{
if (this == object) return true;
if (object == null) return false;
if (getClass() != object.getClass()) return false;
ConditionResult other = (ConditionResult) object;
if (condition == null)
{
if (other.condition != null) return false;
} else if (!condition.equals(other.condition)) return false;
if (result == null)
{
if (other.result != null) return false;
} else if (!result.equals(other.result)) return false;
return true;
}
// Need to implement hashCode as well, for equals consistency...
}
Iteration and counting could be done as:
/**
* Count the instances of condition to result in the supplied results list
*/
public int countInstances(List<ConditionResult> results, String condition, String result)
{
int count = 0;
ConditionResult match = new ConditionResult(condition,result);
for (ConditionResult result : results)
{
if (match.equals(result)) count++;
}
return count;
}

Why remove element of Map?

I am filtering the all list which ahve same lat,long in one list and put into an same list and put that list into map My code is as:-
private Collection<List<Applicationdataset>> groupTheList(ArrayList<Applicationdataset> arrayList)
{
Map<Key, List<Applicationdataset>> map = new HashMap<Key, List<Applicationdataset>>();
for(Applicationdataset appSet: arrayList)
{
Key key = new Key(appSet.getLatitude(), appSet.getLongitude());
List<Applicationdataset> list = map.get(key);
if(list == null){
list = new ArrayList<Applicationdataset>();
}
list.add(appSet);
map.put(key, list);
}
return map.values();
}
public class Key {
String _lat;
String _lon;
Key(String lat, String lon) {
_lat = lat;
_lon = lon;
}
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (!_lat.equals(key._lat)) return false;
if (!_lon.equals(key._lon)) return false;
return true;
}
#Override
public int hashCode() {
int result = _lat.hashCode();
result = 31 * result + _lon.hashCode();
return result;
}
}
But When I am debuging my code according to xml which come from web-service there is 2 list which have same lat long and they are saving in same list in amp at time of debuging but when I go next step of debug the element of map which have 2 item list decrease and showing size 1 I am unable to rectify this issue.
Your code looks OK: You've overridden equals() and hashCode() consistently.
Check for whitespace in the lat/lng values as the cause of your problems, perhaps trim() in the constructor:
Key(String lat, String lon) {
_lat = lat.trim();
_lon = lon.trim();
}
Also, you can simplify your code to this:
#Override
public boolean equals(Object o) {
return o instanceof Key
&& _lat.equals(((Key)o)._lat))
&& _lon.equals(((Key)o)._lon));
}
#Override
public int hashCode() {
// String.hashCode() is sufficiently good for this addition to be acceptable
return _lat.hashCode() + _lon.hashCode();
}
Thats a bit hard to understand what you are trying to accomplish. But I believe the issue is that you are using both latitude and longitude in Key hashCode()/equals() implementation thats why second Applicationdataset in your input list replaces the first one in your map object. You should implement the case when related list was already put into map and do not replace it.

Categories

Resources