Concepts / Object Equality and the equals() Method

Object Equality and the equals() Method

A record declaration with only fields causes the compiler to add a canonical constructor, accessor methods, and definitions of toString(), equals(), and hashCode().

  • Programming

A Four-Line Record That Does a Lot

A Java record lets you describe a simple data holder in very few lines. You name the record, list its fields, and stop. Nothing else is written in the class body, yet a working object can be created, read from, printed, and compared. This raises three questions straight away: where did the constructor come from, where did the methods for reading the fields come from, and what decides how the object prints?

The key idea of this article is that a record declaration is a short request, and the compiler expands it into a fuller class. The simple FullName record used in the examples below is essentially equivalent to a regular class that declares private final fields, a constructor, accessor methods, and definitions of toString(), equals(), and hashCode(). You did not write those parts, but they exist in the compiled meaning of the record.

java

What do you think happens?

You create two separate FullName objects, both holding the first name Ada and the last name Lovelace. Does equals() report them as equal?

  • Yes, because their fields hold the same values

  • No, because they are two separate objects in memory

Reveal answer
Answer:

Yes, because their fields hold the same values

The generated equals() method returns true when the parameter is an object of the same type with the same values for its fields. Being distinct objects in memory does not stop them from being equal in value.

What the Compiler Adds to a Record

Each generated member has a specific job. The diagram below shows the members the compiler supplies for a record declared with only a field list. Notice that every member traces back to the field list you typed, which is why the record's behavior is predictable once you know its fields.

record FullNamefields: firstName, lastNameCanonical constructorone parameter per field, indeclaration orderAccessor methodsfirstName(), lastName()toString()FullName[firstName=...,lastName=...]equals()same type and same fieldvalueshashCode()covered later in the coursegeneratesgeneratesgeneratesgeneratesgenerates
Which members appear automatically when FullName declares only firstName and lastName, and what each one is for?

Tracing a FullName Record from Creation to Printing

The program below uses the FullName record. Read each line and form a guess about its result before checking the output. The point is to notice which parts you typed and which parts the language supplied.

java
Output
Ada
FullName[firstName=Ada, lastName=Lovelace]
Notes on the code
  • The constructor call stores Ada and Lovelace in the matching fields.
  • The accessor firstName() returns the value of the firstName field.
  • Printing the object calls the generated toString() method.
Worked example

Following one value through the record

Starting from new FullName("Ada", "Lovelace"), explain which generated member handles each action on the object.

  1. 1
    Creation

    The canonical constructor receives two arguments in declaration order and stores them in the private final fields.

  2. 2
    Reading the first name

    The call fname.firstName() goes to the accessor, which returns the value held in the firstName field.

  3. 3
    Printing

    Passing fname to System.out.println causes the generated toString() to build FullName[firstName=Ada, lastName=Lovelace].

Result

Each action is served by a generated member, and none of those members had to be written by the programmer.

Identity Versus Equality

The == operator asks a narrower question than many learners expect: do the two expressions refer to the same object in memory? It does not look inside the objects to compare their data. Identity is a fact about where the objects live. Equality is a fact about what they represent. The diagram below shows two references, each pointing at a separate object that holds identical field values.

areferenceFullName objectfirstName=Ada,lastName=LovelacebreferenceFullName objectfirstName=Ada,lastName=Lovelacerefers torefers to
Do a and b refer to one object, or to two separate objects that happen to hold identical field values?

There are therefore three possible situations for two references. They can point to one object, which answers yes to identity and yes to equality. They can point to two objects with the same values, which answers no to identity but yes to equality. Or they can point to objects with different values, which answers no to both. Only the first kind of answer comes automatically from the == operator.

java
Output
false
true
Notes on the code
  • a and b refer to two different objects, so == returns false.
  • equals() compares the field values, which match, so it returns true.

What do you think happens?

After FullName b = a; is the result of a == b true or false?

Reveal answer
Answer:

true

The assignment copies the reference, so a and b now refer to the same single object. Identity holds, so == returns true.

Mistakes Beginners Make with Equality

  • Common mistake

    Assuming that a true result from == means the objects hold the same data

    What it looks like
    FullName a = new FullName("Ada", "Lovelace"); FullName b = new FullName("Ada", "Lovelace"); a == b
    Why it fails

    == tests identity, meaning whether both references refer to the same object. Two separate objects with identical data still fail the == test.

    Do this instead

    Use a.equals(b) when the question is whether the two objects represent the same value.

  • Common mistake

    Expecting a setter on a record and trying to change a field after creation

    Why it fails

    A record's fields are private and final, so the compiler generates no setter methods. A field value cannot be changed once the constructor has assigned it.

    Do this instead

    Create a new record with the desired values. Read values through the accessor methods such as firstName().

  • Common mistake

    Reaching for getFirstName() in a record

    Why it fails

    Record accessors are named after the fields, not with the getXXX() pattern used in ordinary Java classes.

    Do this instead
    Call fname.firstName() and fname.lastName().
  • Common mistake

    Overriding equals() without comparing the fields that define the object's value

Tracing equals() on Two Card Objects

Consider a Card class that stores a suit as a number from 0 to 3 and a value as a number from 1 to 13. Two cards should count as equal when both fields match. The overridden method first converts the incoming object to a Card so that its fields can be read. If the conversion succeeds, it compares the suit and then the value. If the conversion fails, it returns false, because the object cannot possibly be an equal card.

equals(other)calledCast other to Cardsucceeds?Exception caughtNullPointerException orClassCastExceptionreturn falsereturn trueSuit values equal?Value fields equal?return falseyesnoyesnoyesno
Which checks run for a Card compared with another object, and at which step does equals() return true or false?

For a matching pair, every check passes: the conversion succeeds, the suit values are equal, the value fields are equal, and the method returns true. For a mismatched pair, the flow leaves the comparison as soon as one field differs. If the suits match but the values do not, the method returns false at the value check. Follow the diagram line by line for both cases to see exactly which steps run.

The method does not assume that the object it receives is a card. Two situations are named in the source. If the object is null, the cast leads to a NullPointerException. If the object is some other type, the cast leads to a ClassCastException. In both situations the method catches the exception and returns false, because a null reference or a foreign object cannot be equal to a card. Without this handling, an equality check against an unexpected item could crash the program instead of reporting that no match exists.

Locating Where a Card Comparison Goes Wrong

The debugging question is not whether equals() exists, but where in its logic a result departs from what you expected. A correct method can still produce a wrong answer if the comparison reads the wrong fields or stops at the wrong point. The approach is to follow the comparison step by step, note which value is read from which object, and check the answer at each branch. Then locate the exact line where the observed outcome first differs from the intended one.

Where a comparison goes wrong usually falls into one of three places that the flowchart already shows. The first is the cast step, where null and foreign objects must be handled rather than crashing. The second is the field read, where the value must come from the correct object. The third is the early exit, where the method returns a result before every intended field has been compared.

Practice: Tracing a Card Match

EASY

Two Card objects are compared. The first has suit 2 and value 10. The second has suit 2 and value 11. Trace the equals() call through the flowchart. At which check does the method return, and what does it return?

Hints
  • Start by asking whether the cast to Card succeeds. Both objects are cards, so it does.
  • Then check the suit values. Both are 2, so the flow continues to the value check.
  • Compare the value fields. They are 10 and 11, so the flow leaves through the no branch.
MEDIUM

A record is declared as record Card(int suit, int value) {}. Without writing a setter, explain how you would change a card's value from 10 to 11, and identify which generated member you would call to confirm the new card matches another card with suit 2 and value 11.

Hints
  • Records have no setters, so think about creating a new object instead.
  • The constructor takes the suit and value in declaration order.
  • Equality between records uses the generated equals() method.

Key Takeaways

  • A record declared with only a field list causes the compiler to generate a canonical constructor with one parameter per field, accessor methods named after the fields, and definitions of toString(), equals(), and hashCode().
  • Record fields are private and final, so records are immutable and receive no setter methods; a changed value means creating a new object.
  • The == operator tests identity, meaning whether two references refer to the same object, while equals() tests whether two objects represent the same value.
  • An overridden equals() method for a Card compares the object's fields after a successful conversion, and returns false for null or foreign objects rather than crashing.
  • When a comparison gives an unexpected result, follow it step by step to find the first branch or field read where the outcome departs from what was intended.