Chapter 6 - Java for Beginners Course

Object Equality - The Contract

As covered in the inheritance sections, we know that the base class of all classes in Java is the Object class.

One of the inherited methods from the Object class is the equals method that is used to check if the object is equal to an object provided as an input and has the following signature:

public boolean equals(Object obj);

This method should return true if this object is equal to the object provided as a parameter, or false otherwise.

It’s important to note that we’re talking about object equality here and not of object reference equality which was covered in the section about Equality and Relational Operators in Chapter 2.

The equals contract

The documentation of the equals method in the Object class defines a contract that should be followed by any classes that override it.

In summary, the contract specifies 5 main points, the first 3 being the same as the mathematical properties for equality:

  1. Reflective: For a non-null object x, x.equals(x) should return true (an object should be equal to itself).

  2. Symmetric: For 2 non-null objects x and y, x.equals(y) should return true if and only if y.equals(x) returns true.

  3. Transitive: For 3 non-null objects x, y and z, if x.equals(y) is true and y.equals(z) is true then x.equals(z) should return true.

  4. Consistent: For 2 non-null objects x and y, assuming their state hasn’t changed, multiple invocations of x.equals(y) should always return the same value.

  5. Not equal to null: For a non-null object x, x.equals(null) should return false.

The documentation also talks about the relationship with the hashCode method which will be covered in the next section.

equals example

For our example, let’s assume we are using a modified version of the DictionaryEntry class:

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);
    }
}

You’ll notice that our equals method only uses the word field and not the definition as part of the equality checks. The decision on what makes two objects equal depends on the application you’re building, for our example we’ve decided that two DictionaryEntry objects are equal if they represent the same word.

Inside our equals method we are doing the following:

  • The first if statement above covers the reflective case.

  • The second if statement covers the not equal to null case.

  • The third if statement checks if the input object is of the same type, before we do final comparisons.

  • The last lines is where, after casting the input Object into our DictionaryEntry class, we start comparing the state of the objects to make a decision on equality. In our case, we check the word field (via the Objects.equals static method provided by Java).

Most IDEs nowadays provide a way to auto-generate an equals method that follows the required contract.
The Objects class provide a set of static utility methods to do common operations on objects.

Let’s give it a try:

In the example code run the JavaObjectEqualityPropertiesApp
DictionaryEntry run1 = new DictionaryEntry("run", "definition of run");

System.out.println("Reflective property:");
System.out.println("run1 equals itself: " + run1.equals(run1));

DictionaryEntry run2 = new DictionaryEntry("run", "definition of run");
System.out.println("Symmetric property:");
System.out.println("run1 equals run2: " + run1.equals(run2));
System.out.println("run2 equals run1: " + run2.equals(run1));

DictionaryEntry run3 = new DictionaryEntry("run", "definition of run");
System.out.println("Transitive property:");
System.out.println("run1 equals run2: " + run1.equals(run2));
System.out.println("run2 equals run3: " + run2.equals(run3));
System.out.println("run1 equals run3: " + run1.equals(run3));

System.out.println("Not equal to null:");
System.out.println("run1 equals null: " + run1.equals(null));

DictionaryEntry stop = new DictionaryEntry("stop", "definition of stop");
System.out.println("Not equal to different DictionaryEntry: ");
System.out.println("run1 equals stop: " + run1.equals(stop));

Output:

Reflective property:
run1 equals itself: true
Symmetric property:
run1 equals run2: true
run2 equals run1: true
Transitive property:
run1 equals run2: true
run2 equals run3: true
run1 equals run3: true
Not equal to null:
run1 equals null: false
Not equal to different DictionaryEntry:
run1 equals stop: false

Example explained

This is an example of the different properties of the equals method behaving as we expect them to. Here we are using a DictionaryEntry object run1 that represents the word "run" and we’re creating two additional objects run2 and run3 that also represent the same word to illustrate the properties of the equals contract.

We have 3 object references run1, run2 and run3 that point to 3 different objects, this is, the 3 references themselves are different, but the objects they point to are equal based on our application needs (in our case, they represent the same word).

We also create an object stop that represents a different word to illustrate that our equals method behaves as we expect it when we compare objects that represent different words.

However, there are common pitfalls when talking about object equality which we’ll cover in the next section.

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