Chapter 6 - Java for Beginners Course

Object Equality - Common Pitfalls

In the last section we covered the contract of the Object.equals method and we saw an example of an implementation for our DictionaryEntry class.

When writing Java applications there are common pitfalls that come around the concept of object equality and the equals method that we’ll cover in this section.

To start with, let’s assume our DictionaryEntry class has no equals method yet defined:

public class DictionaryEntry {
    private final String word;

    private final String definition;

    public DictionaryEntry(String word, String definition) {
        this.word = word;
        this.definition = definition;
    }

    public final void printEntry() {
        System.out.println(word + ": " + definition);
    }
}

The default behavior of the equals method

The implementation that comes from the Object class returns true if and only if both object references refer to the same object (see Object::equals).

This means, that for two non-null object references x and y, x.equals(y) is equivalent to the object reference equality operation x == y. As mentioned before, this could be the behavior you want, but for some classes it might not be.

For example, let’s create a few objects of our DictionaryEntry class and see what we get:

In the example code run the JavaObjectEqualityApp.
DictionaryEntry word1 = new DictionaryEntry("run", "definition of run");
DictionaryEntry word2 = new DictionaryEntry("stop", "definition of stop");

System.out.println("word1 equals itself: " + word1.equals(word1));
System.out.println("word1 equals word2: " + word1.equals(word2));

Output:

word1 equals itself: true
word1 equals word2: false

Example explained

In the first 2 lines, we are creating 2 objects that have a different state, one that represents the word "run" and one that represents the word "stop". We are then comparing the objects using the equals method, for example, word1.equals(word1).

So far, no surprises, an object is equal to itself (reflective property). And also, the object that represents the word "run" isn’t equal to the object that represents the word "stop".

Now, what happens if we do the following:

DictionaryEntry word1 = new DictionaryEntry("run", "definition of run");
DictionaryEntry word3 = new DictionaryEntry("run", "definition of run");
System.out.println("word1 equals word3: " + word1.equals(word3));

Output:

word1 equals word3: false

In our example, word1 and word3 are two different objects, both represent the word "run" and have the same definition for the word.

However, when we do word1.equals(word3) we don’t get true as a result as one might expect. The reason for this is that our DictionaryEntry class hasn’t overridden the equals method and it is still using the implementation that comes from the Object class (which is equivalent to comparing the object references word1 == word3).

In our case, the two references point to different objects, hence we get false as a result.

Depending on the application and the class, this might not be the behavior you want.

Not following the equals contract

As with any inheritance relationship, we can override the equals method to change the logic for our DictionaryEntry objects (the equals method isn’t final).

In the last section, we discussed we want our DictionaryEntry objects to be considered equal if they represent the same word (we are ignoring the definition).

As such we can define an override for our equals method:

public class DictionaryEntry {
    private final String word;

    // ...

    @Override
    public boolean equals(Object obj) {
        DictionaryEntry other = (DictionaryEntry) obj;
        return word.equals(other.word);
    }

    // ...
}
This equals method shouldn’t be used as a reference example!!

If we run our last example again, we now get:

Output:

word1 equals word3: true

Which is great, now two DictionaryEntry objects are equal if they represent the same word.

However, let’s try checking the not equals to null condition defined in the equals contract:

System.out.println("word1 equals null: " + word1.equals(null));

We should expect to get false based on the contract, however, we get:

Output:

Exception in thread "main" java.lang.NullPointerException
	at io.jcoder.tutorials.ch06.objectequality.DictionaryEntry.equals(DictionaryEntry.java:28)
	at io.jcoder.tutorials.ch06.objectequality.JavaObjectEqualityApp.main(JavaObjectEqualityApp.java:27)

Our current implementation doesn’t handle the null case correctly and it throws an exception.

The contract of equals is relied upon by multiple libraries in Java and should be implemented correctly. The equals method we saw on the previous section follows the contract as expected.

The hashCode method

The last part of the equals method contract talks about the general need to also override the hashCode method when equals is overridden.

If you use an IDE to auto-generate your equals method you should also get an auto-generated hashCode method to comply with the contract of both.

Hash codes will be introduced in more detail in later sections in this chapter when we start introducing hash tables in the data structures section.

For the purpose of this section it is important to understand that:

  1. A hash code is a number that is generated from a subset of the state of an Object. Hence the signature of the method is: public int hashCode().

  2. In general, the state used to generate the hash code is the same as the one used for equality checks, in our DictionaryEntry case the word field.

  3. The hash code could be any number and there are different ways to generate one. However, this number needs to follow the rules below:

    1. If two non-null objects x and y are equal, then their hash code should be the same.

    2. For the same non-null object x, invoking x.hashCode() multiple times should return the same value provided the state used by the equals method isn’t modified.

      1. A small caveat on this rule: if you run the same Java application multiple times, the hash code of an object might be different to the last time you ran the application even if their state is the same. This is perfectly valid as long as the rules are followed while the Java application is running.

    3. If two non-null objects x and y aren’t equal, there is no requirement that their hash code should be different. They can have the same hash code, but ideally, it should be different.

In our case, a hashCode method that complies with the rules above is this one:

@Override
public int hashCode() {
    return Objects.hash(word);
}
Note we are using again the Objects utility class from Java.
As our equals method only relies on the word field, it makes sense for our hashCode method to also only rely on the word field to calculate its hash code.

Putting it all together

We covered a lot of rules in these 2 sections about object equality, which sound complex but in general don’t translate into very complex pieces of code. Our final DictionaryEntry class after doing the changes above is this one:

public class DictionaryEntry {
    private final String word;

    private final String definition;

    public DictionaryEntry(String word, String definition) {
        this.word = word;
        this.definition = definition;
    }

    public final void printEntry() {
        System.out.println(word + ": " + definition);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof DictionaryEntry)) {
            return false;
        }
        DictionaryEntry other = (DictionaryEntry) obj;
        return Objects.equals(word, other.word);
    }

    @Override
    public int hashCode() {
        return Objects.hash(word);
    }
}
Code in GitHub

Get the code for this tutorial using the links below.

Project Repo
Download code for this step
Main class for this step
Dependencies

This is a list of recommended tutorials or courses that might be useful before starting this one.

Contents
Welcome to the Course!
Course Introduction
Chapter 1 - Building Blocks
Quick introduction to Java Variables Classes And Objects Class Example - Defining a class Object Examples - Creating instances Java Application Example - Running our first app Accessing class members - The dot operator Packages - Organizing the code
Chapter 2 - Primitives and Operators
Primitives Arithmetic Operators Assignment Operator Unary Operators Equality and Relational Operators Conditional Operators
Chapter 3 - Statements and Control Flow
Expressions Statements If-Then Statement If-Then-Else Statement More If Statements Switch Statement While and Do-While Statements For Statement Branching Statements Exception Handling
Chapter 4 - Code Example
Example Project - A Simple Vending Machine Adding money Delivering Items Giving Change
Chapter 5 - Classes and Interfaces
Introduction Access Level Modifiers Class Declaration - Class, Methods and Fields Class Declaration - Constructors Inheritance Basics Inheritance - Constructors Inheritance - Methods and Fields Polymorphism Abstract Classes and Methods Interfaces Static Class Members Class Composition Final Classes and Class Members Generic Classes
Chapter 6 - Base Object Behaviors
Introduction Type Comparison Type Casting Object Equality - The Contract Object Equality - Common Pitfalls Object String Representation Garbage Collection Object Comparison Primitive Wrappers and Autoboxing
Chapter 7 - Data Structures
Introduction Arrays - Declaration and Creation Arrays - Basic Operations Core Collection Interfaces List and ArrayList - Basic Operations ArrayList Internals Introduction to Hash Tables Map and HashMap - Basic Operations Set and HashSet - Basic Operations
Chapter 8 - Anonymous classes and lambdas
Introduction Filtering a List Anonymous Classes Lambdas Built-in Functional Interfaces
Chapter 9 - Streams
Introduction Creating Streams Intermediate Operations Terminal Operations