Java object equality

Java object equality

In Java, object equality refers to the state where two objects are considered the same. There are two primary ways to check for equality: reference equality (using ==) and logical equality (using equals()).


Reference Equality (==)

The == operator compares the memory addresses of two objects. This means it checks if two object references point to the exact same object in memory. If they do, the expression object1 == object2 evaluates to true; otherwise, it’s false. For primitive data types (like int, char, double), the == operator compares their actual values.

For example:

String a = new String("hello");
String b = new String("hello");
String c = a;

System.out.println(a == b); // false, as 'a' and 'b' are different objects in memory
System.out.println(a == c); // true, as 'a' and 'c' both refer to the same object

Logical Equality (equals())

The equals() method is used to determine if two objects are logically equal, meaning they have the same content or state. The default equals() method, inherited from the Object class, behaves just like the == operator—it checks for reference equality.

To properly compare the contents of two objects, you must override the equals() method in your class. When overriding equals(), it’s a best practice to also override the hashCode() method, as contractually required by the Java API. The contract states that if two objects are equal according to the equals() method, they must have the same hash code.

For example, a custom equals() method for a Book class would check if the title, author, and ISBN are the same:

public class Book {
    private String title;
    private String author;
    private String isbn;

    // constructor and getters...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Book book = (Book) o;
        return Objects.equals(title, book.title) &&
               Objects.equals(author, book.author) &&
               Objects.equals(isbn, book.isbn);
    }
}

This overridden method allows you to compare two Book objects and determine if they represent the same book, regardless of whether they are the same instance in memory.

java equals and hashcode methods

In Java, the equals and hashCode methods are essential for comparing objects and ensuring they behave correctly in hash-based data structures. They are defined in the java.lang.Object class, which is the superclass of all Java classes.

The equals() Method

The equals() method determines if two objects are considered equal in terms of their content or logical state. The default implementation of equals() in Object simply checks for reference equality using the == operator, meaning it returns true only if both variables refer to the exact same object in memory.

You should override this method whenever you need to define a custom notion of equality for your class. This is common for classes representing data, such as a Person with name and age fields. To correctly override equals(), you must follow these five rules (the equals contract):

  1. Reflexive: x.equals(x) must be true.
  2. Symmetric: If x.equals(y) is true, then y.equals(x) must also be true.
  3. Transitive: If x.equals(y) is true and y.equals(z) is true, then x.equals(z) must also be true.
  4. Consistent: Multiple invocations of x.equals(y) must consistently return the same result, assuming no information used in equals comparisons on the objects is modified.
  5. Nullity: x.equals(null) must always be false.

Here’s an example of a simple equals() override:

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

The hashCode() Method

The hashCode() method returns an integer hash code value for the object. This value is used primarily by hash-based collections like HashSet, HashMap, and Hashtable to quickly store and retrieve objects. The hash code helps distribute objects into different buckets within the collection, which speeds up lookups.

The hashCode contract is a set of rules that must be followed if you override hashCode():

  1. If two objects are equal according to the equals() method, they must have the same hash code.
  2. If an object’s state isn’t changed in a way that affects its equals() comparison, hashCode() must return the same value consistently.
  3. Unequal objects do not need to have different hash codes, although having different hash codes for unequal objects improves the performance of hash collections.

Why override both equals() and hashCode()?

Failing to override hashCode() when you override equals() can lead to significant bugs, especially when using hash collections.

For example, if you add an object to a HashSet, its hashCode() is used to determine its bucket. If you later create a new, but equal, object and try to check if it exists in the HashSet using contains(), the contains() method will call hashCode() on the new object. If hashCode() isn’t overridden, it will return a different value (based on memory address), causing the contains() method to look in the wrong bucket and incorrectly return false.

The golden rule: If you override equals(), you must override hashCode(). A simple way to generate a good hash code is to use the fields that are used in the equals() comparison. The Objects.hash() utility method is a convenient way to do this.

Here is the corresponding hashCode() method for the Person class:

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

Links to this note