Java Top

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

> CHECK OUT THE COURSE

Authors Top

If you have a few years of experience in the Java ecosystem, and you’d like to share that with the community, have a look at our Contribution Guidelines.

1. Overview

In this tutorial, we'll describe two basic equality checks in Java – reference equality and value equality. We'll compare them, show examples, and highlight the key differences between them.

Also, we'll focus on null checks and understand why we should use reference equality instead of value equality when working with objects.

2. Reference Equality

We'll start by understanding reference comparison, which is represented by the equality operator (==). Reference equality occurs when two references point to the same object in the memory.

2.1. Equality Operator With Primitive Types

We know that the primitive types in Java are simple, non-class raw values. When we use the equality operator with primitive types, we're just comparing their values:

int a = 10;
int b = 15;
assertFalse(a == b);

int c = 10;
assertTrue(a == c);

int d = a;
assertTrue(a == d);

As shown above, equality and reference checking work identically for primitives. When we initialize a new primitive with the same value, the check returns true. Moreover, the operator returns the same result if we reassign the origin value to the new variable and compare it.

Let's now perform null checks:

int e = null; // compilation error
assertFalse(a == null); // compilation error
assertFalse(10 == null); // compilation error

Java prohibits assigning null to a primitive. In general, we can't perform any null checks with the equality operator on primitive variables or values.

2.2. Equality Operator With Object Types

As for object types in Java, the equality operator performs a referential equality comparison only, ignoring the object values. Before we implement the tests, let's create a simple custom class:

public class Person {
    private String name;
    private int age;

    // constructor, getters, setters...
}

Now, let's initialize some class objects and inspect the equality operator results:

Person a = new Person("Bob", 20);
Person b = new Person("Mike", 40);
assertFalse(a == b);

Person c = new Person("Bob", 20);
assertFalse(a == c);

Person d = a;
assertTrue(a == d);

The results are quite different than before. The second check returns false while we had got true for the primitives. As we mentioned earlier, the equality operator ignores the internal values of the object when comparing. It only checks that two variables are referencing the same memory address.

Unlike primitives, we can use nulls while working with objects:

assertFalse(a == null);
Person e = null;
assertTrue(e == null);

By using the equality operator and comparing null, we check if the object assigned to the variable is already initialized.

3. Value Equality

Let's now focus on the value equality test. Value equality takes place when two separate objects happen to have the same values or state.

This compares values and is closely related to the Object's equals() method. As before, let's compare its use with primitives and object types, looking at key differences.

3.1. equals() Method With Primitive Types

As we know, primitives are basic types with a single value and don't implement any methods. Therefore, it's impossible to call the equals() method directly using primitives:

int a = 10;
assertTrue(a.equals(10)); // compilation error

However, since every primitive has its own wrapper class, we can use the boxing mechanism to cast it into its object representation. Then, we can easily call the equals() method as if we are using object types:

int a = 10;
Integer b = a;

assertTrue(b.equals(10));

3.2. equals() Method With Object Types

Let's go back to our Person class. For the equals() method to work correctly, we need to override the method in the custom class by considering the fields contained in the class:

public class Person {
    // other fields and methods omitted

    @Override
    public boolean equals(Object o) {
        if (this == o) 
            return true;
        if (o == null || getClass() != o.getClass()) 
            return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }
}

First of all, the equals() method returns true if the given value has the same reference, which is checked by the reference operator. If not, we start the equality test.

Further, we test the equality of the Class objects for both values. We return false if they're different. Otherwise, we continue checking for equality. Finally, we return the combined result of comparing each property separately.

Now, let's modify the previous test and check the results:

Person a = new Person("Bob", 20);
Person b = new Person("Mike", 40);
assertFalse(a.equals(b));

Person c = new Person("Bob", 20);
assertTrue(a.equals(c));

Person d = a;
assertTrue(a.equals(d));

As we can see, the second check returns true as opposed to the reference equality. Our overridden equals() method compares the internal values of the objects.

If we don't override the equals() method, the method from the parent class Object is used. Since the Object.equals() method only does reference equality check, the behavior might not be what we'd expect when comparing Person objects.

While we haven't shown the hashCode() method above, we should note that it's important to override it whenever we override the equals() method to ensure consistency between these methods.

4. Null Equality

In the end, let's check, how the equals() method works with the null value:

Person a = new Person("Bob", 20);
Person e = null;
assertFalse(a.equals(e));
assertThrows(NullPointerException.class, () -> e.equals(a));

When we check it using the equals() method against the other object, we get two different results depending on the order of those variables. The last statement throws an exception because we call the equals() method on the null reference. To fix the last statement, we should first call the equality operator check:

assertFalse(e != null && e.equals(a));

Now, the left side of the condition returns false, making the entire statement equal to false, preventing the NullPointerException from being thrown. Therefore, we must remember to first check that the value on which we are calling the equals() method is not null, otherwise, it can lead to annoying bugs.

Moreover, since Java 7, we can use a null-safe Objects#equals() static method to perform equality checks:

assertFalse(Objects.equals(e, a));
assertTrue(Objects.equals(null, e));

This helper method performs additional checks to prevent throwing the NullPointerException, returning true when both parameters are null.

5. Conclusion

In this article, we discussed reference equality and value equality checks against primitive and object values.

To test for the reference equality, we use the == operator. This operator works slightly differently for primitive values ​​and objects. When we use the equality operator with primitives, it compares values. On the other hand, when we use it with for objects, it checks memory references. By comparing it with a null value, we simply check that the object is initialized in memory.

To perform a value equality test in Java, we use the equals() method inherited from Object. Primitives are simple non-class values, so this method cannot be called without wrapping.

We also need to remember to only call the equals() method on an instantiated object. Otherwise, an exception will be thrown. To prevent this, if we suspect a null value, we should check the value with the == operator.

As always, the source code for the examples is available over on GitHub.

Java bottom

Get started with Spring 5 and Spring Boot 2, through the Learn Spring course:

>> CHECK OUT THE COURSE
Generic footer banner
Comments are closed on this article!