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:

Listing 1. Exemplary immutable domain class in Groovy
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:

Listing 2. The default equals behavior
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)
1We can use named parameters constructor.
2Objects 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.

Listing 3. Shadowing default equals(o) with custom implementation
import 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)
1Some simple equals(o) method generated by IntelliJ IDEA.
2This 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.

Listing 4. Implementing Comparable<Person> interface
import 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)
1The compareTo(person) method implementation that compares ages only.
2Now 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.

Listing 5. Comparing objects reference with a.is(b) method
import 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)
1Prints false.
2Prints 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.

Listing 6. Incorrect usage of Comparable interface
import 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)
1What 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:

Listing 7. Java representation of Groovy == operator
ScriptBytecodeAdapter.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) or a.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, implement Comparable<Object> (or simply Comparable 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.

See you next time.

Szymon Stepniak

Groovista, Upwork's Top Rated freelancer, Toruń Java User Group founder, open source contributor, Stack Overflow addict, bedroom guitar player. I walk through e.printStackTrace() so you don't have to.