How Groovy's equal operator differs from Java?
One of the first mistakes people do when starting their journey with Java programming language is using ==
to compare objects instead calling a.equals(b)
. When you begin playing around with Groovy you quickly notice that equal operator ==
is used to compare objects in place of calling a.equals(b)
. "Finally something more intuitive!" you might think. In today’s article we will dig a little bit deeper to learn avoiding problems equal operator in Groovy may produce.
Example - Person
class
Let’s start with fairly simple example. Below you can find a simple domain class Person
:
import groovy.transform.Immutable
@Immutable
class Person {
String name
int age
}
The @Immutable [1] annotation makes creating immutable classes much easier - it will make the annotated class final, all fields will be final as well, getter methods will be generated + equals and hashCode . |
In this case equals(o)
method gets generated by AST transformation and it simply compares all object properties to decide if they are equal. Let’s see how it works:
import groovy.transform.Immutable
@Immutable
class Person {
String name
int age
}
def john = new Person(name: 'John', age: 32) (1)
def mark = new Person(name: 'Mark', age: 32)
println john == mark (2)
1 | We can use named parameters constructor. |
2 | Objects are not equal, so it prints false to the console. |
Implementing equals(o)
Let’s implement our own equals(o)
method and let’s make it print some text to the console to make sure this particular method got triggered.
equals(o)
with custom implementationimport groovy.transform.Immutable
@Immutable
class Person {
String name
int age
@Override
boolean equals(o) { (1)
println "equals(o) triggered"
if (this.is(o)) return true
if (getClass() != o.class) return false
Person person = (Person) o
if (age != person.age) return false
if (name != person.name) return false
return true
}
}
def john = new Person(name: 'John', age: 32)
def mark = new Person(name: 'Mark', age: 32)
println john == mark (2)
1 | Some simple equals(o) method generated by IntelliJ IDEA. |
2 | This line still prints false to the console. |
The only thing that has changed comparing to the previous example is that equals(o) triggered
shows in the console when we run this example.
Implementing Comparable
interface
There is one exception from the rule that ==
maps to the equals(o)
method. This exception has something to do with Comparable
interface - if class implements it, then ==
maps to the compareTo(o)
method instead[2]. Let’s play around with this use case - we will implement compareTo(o)
in a way it only takes person’s age into account.
Comparable<Person>
interfaceimport groovy.transform.Immutable
@Immutable
class Person implements Comparable<Person> {
String name
int age
@Override
boolean equals(o) { (1)
println "equals(o) triggered"
if (this.is(o)) return true
if (getClass() != o.class) return false
Person person = (Person) o
if (age != person.age) return false
if (name != person.name) return false
return true
}
@Override
int compareTo(Person person) { (1)
println "compareTo(person) triggered"
return this.age <=> person?.age
}
}
def john = new Person(name: 'John', age: 32)
def mark = new Person(name: 'Mark', age: 32)
println john == mark (2)
1 | The compareTo(person) method implementation that compares ages only. |
2 | Now it prints true , because both persons are the same age. |
When we run this example we will also see that only compareTo(person) triggered
a.is(b)
as an equivalent of Java’s ==
If you need to compare objects reference you will have to use Groovy’s a.is(b)
method which translates to the same thing that Java’s ==
does.
a.is(b)
methodimport groovy.transform.Immutable
@Immutable
class Person {
String name
int age
}
def john = new Person(name: 'John', age: 32)
def mark = new Person(name: 'Mark', age: 32)
def johnCopy = john
println john.is(mark) (1)
println johnCopy.is(john) (2)
1 | Prints false . |
2 | Prints true . |
Using Comparable
between incompatible types
There is one use case when implementing Comparable
interface makes equals operator return false
for every comparison. It happens if you implement Comparable
interface with incompatible type (the type that has nothing to do with the class we implement Comparable
for). I know mentioning such use case may sound bizarre to you, however there are some people who actually tried doing it and were surprised it didn’t work as they could expect. It’s hard to come up with some logical example, but let’s say we have a class Profession
and each Person
has a profession - in this case for some reason we want to compare persons with professions and return true
if given person has the profession and false
otherwise.
Comparable
interfaceimport groovy.transform.Immutable
@Immutable
class Profession implements Comparable<Profession> {
static Profession DEVELOPER = new Profession(name: 'Software Developer')
String name
@Override
int compareTo(Profession profession) {
return this.name <=> profession.name
}
}
@Immutable
class Person implements Comparable<Profession> {
String name
int age
Profession profession
@Override
boolean equals(o) {
println "equals(o) triggered"
if (this.is(o)) return true
if (getClass() != o.class) return false
Person person = (Person) o
if (age != person.age) return false
if (name != person.name) return false
if (profession != person.profession) return false
return true
}
@Override
int compareTo(Profession profession) {
println "compareTo(profession) triggered"
return this.profession <=> profession
}
}
def john = new Person(name: 'John', age: 32, profession: Profession.DEVELOPER)
println john == Profession.DEVELOPER (1)
1 | What do you think - does it print true or false ? |
The above example compiles and runs without any issue. If we implemented it that way and we expect that john == Profession.DEVELOPER
evaluates to true
, we will be surprised. If we run it we will notice that compareTo(profession) triggered
is not printed to the console, neither the equals(o) triggered
. What is printed to the console is false
. If none of these two methods got triggered, then how does Groovy decided that john
is not equal Profession.DEVELOPER
?
The answer to this question can be found in understanding how Groovy executes a.compareTo(b)
method. If compareTo
gets executed between two different types, Groovy uses DefaultTypeTransformation.compareTo(left, right)
method that tries to cast both sides to a common type so it can perform compareTo
between them. Otherwise it simply returns -1
and that’s it.
Special use case: comparing object references
There is one special use case where Groovy ==
operator behaves exactly the same as Java - it happens when you compare object1 == object1
. How is this possible? You have to understand that Groovy translates left == right
to something like this:
==
operatorScriptBytecodeAdapter.compareEqual(left, right)
If we take a quick look at the source code of this method we will notice that in the first line it does:
if (left==right) return true;
It means that in case of comparing a references to the same object, Groovy does not trigger equals(o)
or compareTo(o)
methods, but it simply returns true
, similarly to what Java does in such case.
I have provided even more detailed explanation of this use case in the following Stack Overflow answer. |
Conclusion
I hope you have learned something interesting from this article. Let’s sum it up with the following:
Use
==
to compare objects, but be aware what might happen under the hood.You can always call
a.equals(b)
ora.compareTo(b)
directly if you don’t want to Groovy decide for you which method should be executed.If you want to compare object
a
with wide variety of different types, implementComparable<Object>
(or simplyComparable
with any generic type) and make all casts between types implicit.If you read this article up to this point - thank you very much! Don’t hesitate to leave a comment and tell others what is your favorite Groovy feature.
0 Comments