Equals, Is, CompareTo, and the Groovy Identity Operator
If you’ve ever tried to determine if Object a
is the same as Object b
in Groovy, chances are you’ve thought a lot about a == b
and a.equals(b)
and a.is(b)
. To appropriately ask this question of two objects in Groovy, it’s important to understand the behavior of these three operations and the difference between the equals operator in Groovy vs Java. I’m about to walk through these behaviors, that difference, and emphasize that it’s just as important to understand a fourth operation, a.compareTo(b)
.
This GitHub repository will come in handy eventually: https://github.com/burkaa01/groovy-equals
Let’s start in Java
In Java equals()
is used for value comparison while ==
is used for reference comparison. Take a look at the following example:
String s1 = new String("s"); String s2 = new String("s"); s1 == s2 // false s1.equals(s2) // true
s1 == s2
evaluated to false
because s1
and s2
did not refer to the same underlying object, while s1.equals(s2)
evaluated to true
because s1
and s2
were value equivalent. Let’s look at another example:
String s3 = s1; s1 == s3 // true s1.equals(s3) // true
s1 == s3
evaluated to true
this time because the two object references referred to the same underlying object.
Quick note about primitive types
It is appropriate to compare the values of primitive types using ==
and in cases like this the ==
operation in Java behaves the same as it will in Groovy:
int i1 = 100; int i2 = 100; i1 == i2 // true
Time for some Groovy
In Groovy equals()
is also used for value comparison and behaves the same as it does in Java. The ==
operation does not behave the same as it does in Java and instead is()
is used for reference comparison. The following example is the Groovy equivalent of that first Java example:
String s1 = new String("s") String s2 = new String("s") s1.is(s2) // false s1.equals(s2) // true
s1.is(s2)
evaluated to false
because s1
and s2
did not refer to the same underlying object, while s1.equals(s2)
evaluated to true
because s1
and s2
were value equivalent.
What about == in Groovy?
In Groovy ==
is also used for value comparison, but it is a common misconception that ==
behaves exactly as equals()
does. Take a look at how the Groovy Language Documentation describes the identity operator:
Identity operator In Groovy, using == to test equality is different from using the same operator in Java. In Groovy, it is calling equals.
See http://docs.groovy-lang.org/docs/latest/html/documentation/#_identity_operator
This description fails to clarify something that it should: ==
is not always calling equals()
. Thankfully, a different portion of the Groovy Language Documentation saves the day:
Behaviour of == In Java == means equality of primitive types or identity for objects. In Groovy == translates to a.compareTo(b)==0, if they are Comparable, and a.equals(b) otherwise.
See http://docs.groovy-lang.org/latest/html/documentation/#_behaviour_of_code_code
Let’s demonstrate that extremely important caveat:
class BrokenString extends GString { // GString because final class String cannot be extended String str BrokenString(String str) { super(new String[0]) this.str = str } String[] getStrings() { return [str] } boolean equals(BrokenString o) { // broken equals implementation return false } } BrokenString s1 = new BrokenString('s') BrokenString s2 = new BrokenString('s') s1 // s s2 // s s1 == s2 // true s1.equals(s2) // false
s1 == s2
evaluated to true
while s1.equals(s2)
evaluated to false
. We broke the equals()
implementation and ==
must not have called equals()
because they behaved differently. This is because our BrokenString (an extension of groovy.lang.GString) implemented Comparable (or rather groovy.lang.GString implemented Comparable) and we just learned that in Groovy ==
translates to a.compareTo(b)==0
if they are Comparable. Let’s adjust BrokenString and prove that:
class BrokenString extends GString { // GString because final class String cannot be extended String str BrokenString(String str) { super(new String[0]) this.str = str } String[] getStrings() { return [str] } int compareTo(Object s) { // broken compareTo implementation return 1 } } BrokenString s1 = new BrokenString('s') BrokenString s2 = new BrokenString('s') s1 // s s2 // s s1 == s2 // false s1.equals(s2) // true
We repaired the equals()
implementation, but this time, when we broke the compareTo()
implementation, we also broke ==
.
Why should we care?
Now would be a good time to take a closer look at that GitHub repository: https://github.com/burkaa01/groovy-equals
Ridiculous BrokenString example aside, you should seriously consider this behavior in Groovy when you choose to implement Comparable (a behavior you do not need to consider when you implement Comparable in Java). Let’s take a closer look at the Java Documentation for the Comparable interface:
It is strongly recommended, but not strictly required that (x.compareTo(y)==0) == (x.equals(y))
See https://docs.oracle.com/javase/8/docs/api/java/lang/Comparable.html
(x.compareTo(y)==0) == (x.equals(y))
may not be strictly required, but we just learned that in Groovy if (x.compareTo(y)==0) != (x.equals(y))
when you implement Comparable, you will effectively break ==
.
Suppose we have the following Groovy class that implements Comparable:
@EqualsAndHashCode class GroovyEmployee implements Comparable { int id String firstName String lastName String email @Override int compareTo(Object o) { GroovyEmployee e = (GroovyEmployee) o int byLast = this.lastName <=> e.lastName return byLast ?: this.firstName <=> e.firstName } }
And supposed we have the following GroovyEmployees defined:
jim // Halpert, Jim, id: 440, email: jhalp1@office.com jimSameReference // Halpert, Jim, id: 440, email: jhalp1@office.com jimSameValues // Halpert, Jim, id: 440, email: jhalp1@office.com jimSameNameDifferentValues // Halpert, Jim, id: 123, email: jhalp2@office.com
Notice that for our GroovyEmployee we decided to only consider the firstName
and the lastName
of the employee in compareTo()
because we only implemented Comparable so that we could sort our GroovyEmployees by last name. Look at what that does to jim == jimSameNameDifferentValues
below:
jim.is(jim) // true jim.is(jimSameReference) // true jim.is(jimSameValues) // false jim.is(jimSameNameDifferentValues) // false jim.equals(jim) // true jim.equals(jimSameReference) // true jim.equals(jimSameValues) // true jim.equals(jimSameNameDifferentValues) // false jim == jim // true jim == jimSameReference // true jim == jimSameValues // true jim == jimSameNameDifferentValues // true
jim == jimSameNameDifferentValues
evaluated to true
even though jim
and jimSameNameDifferentValues
do not have the same id
and email
. An equivalent implementation of GroovyEmployee in Java, wouldn’t have this problem:
jim.equals(jim) // true jim.equals(jimSameReference) // true jim.equals(jimSameValues) // true jim.equals(jimSameNameDifferentValues) // false jim == jim // true jim == jimSameReference // true jim == jimSameValues // false jim == jimSameNameDifferentValues // false
What can we do?
Option 1, whenever we implement Comparable in Groovy we could consider every field value in compareTo()
and always make sure (x.compareTo(y)==0) == (x.equals(y))
. Option 2, if we only implemented Comparable so that we could sort our GroovyEmployees by last name, we could use a Comparator instead.
Comparator instead of Comparable
Let’s update our GroovyEmployee:
@EqualsAndHashCode class GroovyEmployee { int id String firstName String lastName String email }
Now the Comparator:
class GroovyEmployeeComparator implements Comparator { @Override int compare(GroovyEmployee e1, GroovyEmployee e2) { int byLast = e1.lastName <=> e2.lastName return byLast ?: e1.firstName <=> e2.firstName } }
End result: the same sort order, but now jim == jimSameNameDifferentValues
will evaluate to false
. Take a look:
jim.is(jim) // true jim.is(jimSameReference) // true jim.is(jimSameValues) // false jim.is(jimSameNameDifferentValues) // false jim.equals(jim) // true jim.equals(jimSameReference) // true jim.equals(jimSameValues) // true jim.equals(jimSameNameDifferentValues) // false jim == jim // true jim == jimSameReference // true jim == jimSameValues // true jim == jimSameNameDifferentValues // false