Why does not Java HashSet.equals() check for object equality? - java

I've come to this phenomenon this morning, the equals method in Set does not check for value equality of the element while List does. This is not in accordance with the java doc.
Set<MyClass> s1 = new HashSet<>();
Set<MyClass> s2 = new HashSet<>();
Set<MyClass> s3 = new HashSet<>();
Set<MyClass> s4 = new HashSet<>();
List<MyClass> l1 = new ArrayList<>();
List<MyClass> l2 = new ArrayList<>();
MyClass o1 = new MyClass();
MyClass o2 = new MyClass();
// **this gives false, and does not call MyClass.equals().**
s1.add(o1);
s2.add(o2);
boolean setCompareWithDifferentObjects = s1.equals(s2);
// this gives true, and also does not call MyClass.equals().
s3.add(o1);
s4.add(o1);
boolean setCompareWithSaveObjects = s3.equals(s4);
// this give true, and MyClass.equals() is called.
l1.add(o1);
l2.add(o2);
boolean listCompare = l1.equals(l2)
I've done some research. According to this
Java doc for Set, HashSet equals , HashSet containsAll, HashSet contains, it will use (o==null ? e==null : o.equals(e)) to check whether the elements are equal. So why is this happen? Can anyone give me some hint on this?
Thanks!
----------Answer to this question can be found here -----------
What issues should be considered when overriding equals and hashCode in Java?
I overrided equals() but not hashCode()...
btw, the same set comparison worked in groovy, even if hashCode() is not overriden.

HashSet includes a number of optimizations that can explain all of this: first, if two objects are put into different buckets by their hash codes, or if they have different hash codes at all, they may skip the equals call. This is allowed by the contract of Object.hashCode; if two objects have different hash codes then they are not allowed to .equals to each other.
For the other case, HashSet takes advantage of the contract of .equals that specifies that if two objects are == to each other, then they must be .equals to each other. Part of HashSet's implementation here checks if the elements are ==, and if they are, it skips calling .equals.
If every method implements its contract correctly, this cannot change the semantics; HashSet will always behave exactly as if .equals were being called.

Related

How to get a unique hashCode for an ArrayList?

var arr1 = new ArrayList<Integer>();
var arr2 = new ArrayList<Integer>();
System.out.println(arr1.hashCode());
System.out.println(arr2.hashCode());
System.out.println(arr1.equals(arr2));
Output:
1
1
true
After searching, I found this answer which explains why this is happening.
From the answer:
Here's the Java 8 implementation (it's actually implemented in
AbstractList) :
public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
And since the two arrays are empty, it's obvious that the hashCode function would return 1 for both of the arrays.
My question is, given two ArrayLists, how to determine whether they are the same or not? How can I be sure that modifing one ArrayList won't affect the other? since I have no unique hash for each ArrayList, I can't compare their hashes to know whether they're equal or not. How to get a unique hash for each object?
Also why the equals function returns true? Does it compare objects by hashes? Are they actually the same object? If yes then why and how to make different objects then?
And what are the classes other than ArrayList that acts in this way?

LinkedList and TreeMap: compareTo or equals?

I need a clarification regarding the use of TreeMap and LinkedList. Do these two structures use compareTo or equals?
In particular, TreeMap keeps the order in the keys, I suppose using the compareTo method of the class defined for the keys. However, when using get, do they use compareTo or equals to see if the key you pass is contained?
I have the same doubt for contains and getIndex inside LinkedList.
TreeMap uses compareTo, and the documentation warns you of problems if compareTo is not consistent with equals (i.e. that a.compareTo(b) == 0 <=> a.equals(b) should be true).
Note that the ordering maintained by a tree map ... must be consistent with equals if this sorted map is to correctly implement the Map interface.
LinkedList uses equals.
The reason why TreeMap has to use an ordering consistent with equals is that the contract of Map defines the behavior in terms of equals. For example, containsKey is defined as:
returns true if and only if this map contains a mapping for a key k such that (key==null ? k==null : key.equals(k))
Let's say you define a class like this:
class Bad implements Comparable<Bad> {
#Override public int compareTo(Bad other) { return 0; }
}
If you were to write:
Bad b1 = new Bad();
Bad b2 = new Bad();
Then:
Map<Bad, String> hm = new HashMap<>();
hm.put(b1, "");
System.out.println(hm.containsKey(b2)); // false
whereas
Map<Bad, String> tm = new TreeMap<>();
tm.put(b1, "");
System.out.println(tm.containsKey(b2)); // true
despite the fact that
System.out.println(tm.keySet().stream().anyMatch(k -> k.equals(b2))); // false
Thus, TreeMap violates the contract of Map, because Bad does not implement Comparable consistently with equals.
The Javadoc of TreeMap and LinkedList answers this:
V java.util.TreeMap.get(Object key)
Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
More formally, if this map contains a mapping from a key k to a value v such that key compares equal to k according to the map's ordering, then this method returns v; otherwise it returns null. (There can be at most one such mapping.)
and
boolean java.util.LinkedList.contains(Object o)
Returns true if this list contains the specified element. More formally, returns true if and only if this list contains at least one element e such that (o==null ? e==null : o.equals(e)).
So, for TreeMap the Comparator\Comparable implementation is used to determine equality of keys, while for LinkedLists equals is used.

List.equals is false despite all objects in list being equal

I have a list of objects, say Article, which is quite a complex, nested object. I will omit the actual class as it shouldn't matter really; it is probably important to know that Article has equals() implemented (with AutoValue).
I have two lists:
List<Article> list1 = getSomeArticles();
List<Article> list2 = getOtherArticles();
Now I check if they are equal:
boolean listsAreEqual = list1.equals(list2);
this returns false.
But I check if a. both lists have the same size and b. each item at index i is equal in both lists:
if (list1.size() != list2.size()) {
return;
}
for (int i = 0; i < list1.size(); i++) {
Article article1 = list1.get(i);
Article article2 = list2.get(i);
if (!article1.equals(article2)) {
return;
}
}
// All items in the two lists are equals, but list2.equals(list2) is false
Both lists seem to contain exactly the same items, but list1.equals(list2) returns false.
How can that be?
They should be equal. Something is not right with your code. From the documentation:
Compares the specified object with this list for equality. Returns
true if and only if the specified object is also a list, both lists
have the same size, and all corresponding pairs of elements in the two
lists are equal. (Two elements e1 and e2 are equal if (e1==null ?
e2==null : e1.equals(e2)).) In other words, two lists are defined to
be equal if they contain the same elements in the same order. This
definition ensures that the equals method works properly across
different implementations of the List interface.
Unless, of course, the List implementation overrides the default from java.util.List.

LinkedHashSet .equals() vs LinkedList .equals() with same elements but different order

Consider the following SSCCE:
public static void main(String[] args) {
LinkedHashSet<String> set1 = new LinkedHashSet<>();
set1.add("Bob");
set1.add("Tom");
set1.add("Sam");
LinkedHashSet<String> set2 = new LinkedHashSet<>();
set2.add("Sam");
set2.add("Bob");
set2.add("Tom");
System.out.println(set1);
System.out.println(set2);
System.out.println(set1.equals(set2));
}
This prints:
[Bob, Tom, Sam]
[Sam, Bob, Tom]
true
Yet if you change LinkedHashSet to LinkedList:
public static void main(String[] args) {
LinkedList<String> set1 = new LinkedList<>();
set1.add("Bob");
set1.add("Tom");
set1.add("Sam");
LinkedList<String> set2 = new LinkedList<>();
set2.add("Sam");
set2.add("Bob");
set2.add("Tom");
System.out.println(set1);
System.out.println(set2);
System.out.println(set1.equals(set2));
}
it produces:
[Bob, Tom, Sam]
[Sam, Bob, Tom]
false
My question is one of clarification. Can someone help make sense of this? Why would a LinkedHashSet be considered equals whereas the same LinkedList would not? I'm assuming the definition of List and Set plays a role, but I'm not sure.
Basically, I'm saying if you consider the Sets to be the same, wouldn't you consider the Lists to be the same too? And vice-versa (assuming no duplicate elements)?
The guarantee that LinkedHashSet makes is about iteration order. However, it's still a Set and a set doesn't care about order in itself. A List on the other hand, does. A List with an element in 3rd position is not the same as another List with the same element in the 1st position.
Set javadoc for the equals(Object) method
Returns true if the specified object is also a set, the two sets have
the same size, and every member of the specified set is contained in
this set (or equivalently, every member of this set is contained in
the specified set). This definition ensures that the equals method
works properly across different implementations of the set interface.
The LinkedHashSet javadoc states
Hash table and linked list implementation of the Set interface, with
predictable iteration order.
A LinkedHashSet is a Set. It has the same rules, ie. those that apply to the set ADT.
As mentioned above:
LinkedHashSet extends HashSet which extends AbstractSet which implements equals method: https://docs.oracle.com/javase/8/docs/api/java/util/AbstractSet.html#equals-java.lang.Object-
Compares the specified object with this set for equality. Returns true if the given object is also a set, the two sets have the same size, and every member of the given set is contained in this set. This ensures that the equals method works properly across different implementations of the Set interface.
The easiest way to compare LinkedHashSet if order if important to you is to serialize it and compare them then:
LinkedHashSet<Integer> reverseOrder = new LinkedHashSet<>();
reverseOrder.add(2);
reverseOrder.add(1);
LinkedHashSet<Integer> ordered = new LinkedHashSet<>();
ordered.add(1);
ordered.add(2);
System.out.println("Equals via set: " + ordered.equals(reverseOrder));
System.out.println("Equals With Arrays: " + ordered.ordered.toString().equals(reverseOrder.ordered.toString()));
Result:
Equals via Set: true
Equals With Arrays: false

How to insert array into hashtable w/o initializing in Java?

I have a hashmap initialized as follows:
Hashmap<String[][], Boolean> tests = new Hashmap<String [][], Boolean>();
I would like to insert into tests without having to initialize the key:
tests.put({{"a"}, {"a"}}, true);
However, Java doesn't seem to let me do this. It works if I do it like this:
String[][] hi = {{"a"}, {"a"}};
tests.put(hi, true);
Is there any way to avoid the latter and get the former working?
Can someone also explain the reasoning behind this error?
Thanks
Yes, you can write like this:
tests.put(new String[][] {{"a"}, {"a"}}, true);
This is often referred to as an anonymous array or a just-in-time array.
In your case you would have to use
tests.put(new String[][]{{"a"}, {"a"}}, true);
because as you noticed {{"a"}, {"a"}}
String[][] hi = {{"a"}, {"a"}};
can be used only while creating reference to array.
You can use
tests.put(new String[][]{{"hello", "goodbye"},{"hi", "bye"}}, true);
This is almost definitely not what you want.
Arrays in Java get their equality and hash code from Object -- which is to say, based on their reference identity. So:
String[] a = { "hello" }; // create one array
String[] b = { "hello" }; // create a different array with the same contents
assert a != b; // the two references are to different objects
assert ! a.equals(b); // they're not equal
assert a.hashCode() != b.hashCode(); // neither are their hashes (probably)
a and b will not be equal, and their hash codes will almost certainly not be equal, since they are different objects. This means that if you use an array as the key to a hash map, you won't be able to retrieve the value using an key but the exact one that you created it with: any other array will have a different hash code and will be non-equal, and therefore won't be considered an equivalent key.
The solution is to replace the String[][] with a List<List<String>>. Lists define equality and hash codes based on their contents, so a list containing [ "hello" ] is equal to any other list containing [ "hello" ]:
List<String> x = Arrays.asList("hello");
List<String> y = Arrays.asList("hello");
assert x != y; // the two lists are different objects
assert x.equals(y); // but they're equal
assert x.hashCode() == y.hashCode(); // and so are their hash codes
Now you can use the lists as keys. Keep in mind that once a list is a key to the map, it's not allowed to change values. Doing so will probably break the hash map, because the list's hash code will have changed, but the map won't know about it, so the map will look for it in the wrong hash bucket.
The easiest options here are:
be sure that nobody else has a reference to that same List object and might change it
create a deep copy of the List before you put it into the map (that is, copy the "inner" lists as well as the "outer" one)
For the second option, it'd be something like:
// copy the outer list
List<List<String>> outerCopy = new ArrayList<List<String>>( originalList );
ListIterator<List<String>> listIterator = outerCopy.listIterator();
while (listIterator.hasNext()) {
// make a copy of the inner list
List<String> innerCopy = new ArrayList<String>( listIterator.next() );
listIterator.set(innerCopy);
}

Categories