How to correctly compare objects in Java

In an object-oriented programming language like Java, comparing objects to each other or comparing them to other primitive types is a commonly performed fundamental task. From practice, however, we know that especially novice programmers have difficulty correctly comparing data and knowing when to use the equality operators (==) and inequality (!=) and when to use the methods equals(), or compareTo(), which can be quite confusing for them.

Object comparison is the process of checking whether two objects are the same or not based on data or references. In Java, objects are created from classes and each object has its own set of data and defined behavior. However, we can also compare whether the objects occur at the same location in memory, and if so, they are not two objects, but one and the same.

In this article, we will show how to compare objects in Java correctly, either based on the data in the objects or their references, using practical examples. You will also learn how the equals() method is implemented and works, and we will also mention the importance of implementing the hashCode() method for efficient comparison of objects using the hash algorithm.

Comparing objects with primitive types

In Java, primitive types such as int, double, boolean, etc. are not objects, but basic data types. When comparing objects with primitive types, Java automatically converts the corresponding wrapper object of a class to its primitive type using the extension primitive conversion(§5.1.2). That has the following rules specified:

  • If one of the operands is of type double, the other one will be converted to double.
  • Otherwise, if one of the operands is of type float, the other is converted to float.
  • Otherwise, if one of the operands is of type long, the other is converted to long.
  • Otherwise, both operands are converted to type int.

Example:

Integer I = new Integer(5);
int i = 5;

System.out.println(I == i);
// => true

System.out.println(I.equals(i));
// => true

If we compare an object with its primitive type, either using the == operator or equals method, it is equivalent, and if the values are the same, the result is a boolean value true.

Comparing objects based on references

The identity of a variable (also called a reference equality) is defined by the reference it holds. If two variables have the same reference, they are identical . This is controlled by the == operator.

It is important to remember that the == operator is used in Java to compare the references of two objects, not their contents. Thus, it checks whether both references point to exactly the same object in memory (or its address).

Example:

String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1 == s2);
// => false

In this example, both strings have the same text, but since we are comparing references (memory addresses of two different instances), the result is understandably a boolean value false.

Example:

String s1 = new String("Java");
String s2 = s1;
System.out.println(s1 == s2);
// => true

However, if instead of creating an instance in the second string, we copy the reference to the first string when comparing references, the result is true.

Comparing objects based on values

The equality of a variable is defined by the value it references. If two variables refer to the same value, they are the same. This is checked in Java using the equals() method. It is part of the Object class, which is the parent class of all Java classes. This means that we can use equals in Java to compare the data content of any two objects.

Example:

String s1 = new String("Java");
String s2 = new String("Java");
System.out.println(s1.equals(s2));
// => true

The example demonstrates comparing 2 different instances of the String type with the same text. The result of the equals() call returns the boolean value true. If we had a copied reference to the same object in the previous example, the result of comparing the values of the same object would also be true.

Example:

String s1 = new String("Java");
String s2 = s1;
System.out.println(s1.equals(s2));
// => true

Until now, we have been using simple objects in the examples that the Java authors have prepared for us in the libraries. Now let’s try creating a custom object and look at the equals() method in more detail.

Example:

Shape class

package pack;

public class Shape {
    String colour;

    public Shape(String colour) {
        this.colour = colour;
    }

    public String getColour() {
        return colour;
    }

    public void setColour(String colour) {
        this.colour = colour;
    }
}

Class Circle

package pack;

public class Circle extends Shape {
    double radius;

    public Circle (String colour, double radius) {
        super(colour);
        this.radius = radius;
    }

    public double getRadius() {
        return radius;
    }

    public void setRadius(double radius) {
        this.radius = radius;
    }
}

We have created a Circle class that inherits from the parent class Shape. Now we will create two instances with the same data and compare their references and their values using the equals method.

Circle c1 = new Circle("green", 1);
Circle c2 = new Circle("green", 1);

System.out.println(c1 != c2);
// => true

System.out.println(c1.equals(c2));
// => false

While, the fact that the references to the two different instances are different is what we expected. The surprise, however, may be that even though both objects contain the same attribute values, the equals method returns false and thus the values of the two objects are different. This is because the standard equals implementation in the Object class that every object inherits compares the memory addresses of objects, so it works just like the == operator. Therefore, we need to rewrite this method to define what equality means for our objects. But first, we need to know the rules that the equals method follows.

Equals() method rules

Suppose we have defined x, y, z references. Then a correctly implemented equals method should have the following properties.

Reflexivity: for every non-zero reference x, x.equals(x) should return true.

Symmetry: for all non-zero references x and y, x.equals(y) should return true if and only if y.equals(x) returns true.

Transitivity: for all non-zero x, y and z references, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.

Consistency: for any non-zero reference x and y, multiple invocations of x.equals(y), assuming that no information used in the equals comparison of the objects is changed, will consistently return the same true or false values in succession.

For any reference value that is not null, x.equals(null) should return false.

Correct implementation of the equals() method

Now that we know the rules for equals, let’s implement our own version of equals for the Circle class as follows.

@Override
public boolean equals(Object o) {
    // self check
    if (this == o) return true;

    // Null check
    if (o == null) return false;

    // Type check and cast
    if (getClass() != o.getClass())
        return false;
    
    Circle circle = (Circle) o;

    // Field comparison
    return Objects.equals(radius,  circle.radius)
            && Objects.equals(colour, circle.colour);
}

We’ll check that both instances contain the same values and that the equals method works correctly and returns true.

Circle c1 = new Circle("green", 1);
Circle c2 = new Circle("green", 1);
System.out.println(c1.equals(c2));
// => true

In our implementation of equals, we’ll first check if we’re comparing the object with itself, and if so, the result is true.

Then we check if the reference to the second object with which we are comparing the first object is valid (that is, non-zero) and if not, we return false. No instance should be equal to null, and this line will ensure that we don’t get a NullPointerException when accessing attributes later.

Before comparing the attributes, we test that the two objects being compared belong to the same class. This will also ensure that even if the Circle class is separated from the Shape class, comparing these classes will always return false.

Method hashCode()

A common practice in Java is that if we override the original implementation of equals(), we should also override the hashCode() method, since both methods work together, especially when dealing with collections of objects.

The connection between the two methods is that if the equals method determines that the two objects are the same, then they should also compute the same hash code. Without a correct implementation of the hash code, using collections that depend on it such as HashSet and HashMap will not work correctly.

Now we’ll demonstrate on our Circle class how we should correctly overload the hashCode() method.

@Override
public int hashCode() {
    return Objects.hash(radius, colour);
}

Summary

In Java, comparing two objects is not as simple as comparing two primitive data types. It requires a deeper understanding of the structure of objects as well as how they are stored in memory. The equals() method is the primary method used to compare two objects in Java. When we override the equals() method, we must also override the hashcode() method to ensure that two identical objects have the same hash code. Using the equals() and hashcode() methods, we can effectively compare two objects based on their internal state, rather than their location in memory.

If you’re a Java developer looking for a job, check out our employee benefits and respond to our job offers.

About the author

Jozef Wagner

Java Developer Senior

Viac ako 10 rokov programujem v Jave, momentálne pracujem v msg life Slovakia ako Java programátor senior a pomáham zákazníkom implementovať ich požiadavky do poistného softvéru Life Factory. Vo voľnom čase si rád oddýchnem v lese, prípadne si zahrám nejakú dobrú počítačovú hru.

Let us know about you