Professional Java Practices

Equality, Hashing, and Object Contracts

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

Java-তে দুইটি object দেখতে একই রকম হলেই তারা automatically equal হয় না।

Example:

CourseCode first =
        new CourseCode(
                "JAVA-OOP"
        );

CourseCode second =
        new CourseCode(
                "JAVA-OOP"
        );

Business perspective থেকে আমরা হয়তো বলব:

দুইটি CourseCode একই value represent করে

কিন্তু Java-কে সেটা explicitly শেখাতে হয়।

এই জায়গায় আসে:

==
equals()
hashCode()
toString()

এগুলো শুধু interview topic নয়।

এগুলো সরাসরি affect করে:

List.contains()
Set
HashSet
Map
HashMap
duplicate detection
testing
logging
debugging

এই lesson-এ আমরা শিখব:

  • Reference equality
  • Logical equality
  • ==
  • equals()
  • hashCode()
  • equals() contract
  • hashCode() contract
  • HashSet এবং HashMap কীভাবে equality ব্যবহার করে
  • Value object equality
  • Entity equality considerations
  • Mutable equality fields-এর danger
  • Objects.equals()
  • Objects.hash()
  • toString()
  • Common equality bugs

Learning Objectives

এই lesson শেষে আপনি পারবেন:

  • == এবং equals() distinguish করতে
  • Logical equality define করতে
  • Correct equals() implement করতে
  • Correct hashCode() implement করতে
  • HashSet এবং HashMap-এর সঙ্গে equality-এর relationship explain করতে
  • Immutable value-object equality design করতে
  • Mutable hash key-এর problem explain করতে
  • Useful toString() implement করতে
  • Broken equality implementations identify করতে

Reference Equality

Java object variables সাধারণত objectটিকে directly contain করে না।

তারা একটি reference hold করে।

Example:

CourseCode first =
        new CourseCode(
                "JAVA-OOP"
        );

Conceptually:

first
  ↓
CourseCode object

== with Objects

Object references-এর ক্ষেত্রে:

==

checks whether both references point to the same object।

Example:

CourseCode first =
        new CourseCode(
                "JAVA-OOP"
        );

CourseCode second =
        first;

System.out.println(
        first == second
);

Output:

true

Both variables point to the same object।


Different Objects with the Same Value

CourseCode first =
        new CourseCode(
                "JAVA-OOP"
        );

CourseCode second =
        new CourseCode(
                "JAVA-OOP"
        );

System.out.println(
        first == second
);

Output:

false

because these are two separately created objects।


Logical Equality

Business logic may care about value rather than object identity।

For:

CourseCode("JAVA-OOP")
CourseCode("JAVA-OOP")

we usually want:

Logically equal

This is what:

equals()

is for।


Default equals()

Every Java class inherits:

equals()

from:

Object

The default implementation behaves similarly to reference identity।

So without overriding equals():

first.equals(
        second
)

may still return:

false

for different objects containing the same fields।


Value Object Equality

Consider:

CourseCode

Its meaning comes from:

value

Therefore:

new CourseCode(
        "JAVA-OOP"
)

should usually equal another:

new CourseCode(
        "JAVA-OOP"
)

This is called:

value equality

Implementing equals()

Example:

@Override
public boolean equals(
        Object other
) {
    if (this == other) {
        return true;
    }

    if (
            !(other
            instanceof CourseCode courseCode)
    ) {
        return false;
    }

    return value.equals(
            courseCode.value
    );
}

Step 1: Same Reference

if (this == other) {
    return true;
}

If both references point to the same object, they are obviously equal।

This is a fast path।


Step 2: Type Check

if (
        !(other
        instanceof CourseCode courseCode)
) {
    return false;
}

This handles:

null
different class/type

If other == null, instanceof returns:

false

Step 3: Compare Equality Fields

return value.equals(
        courseCode.value
);

This defines logical equality based on:

CourseCode.value

Complete CourseCode

package io.liveklass.course;

import java.util.Locale;
import java.util.Objects;

public final class CourseCode {

    private final String value;

    public CourseCode(
            String value
    ) {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        this.value =
                value.strip()
                        .toUpperCase(
                                Locale.ROOT
                        );
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                !(other
                instanceof CourseCode courseCode)
        ) {
            return false;
        }

        return value.equals(
                courseCode.value
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                value
        );
    }

    @Override
    public String toString() {
        return value;
    }
}

Normalization Simplifies Equality

Constructor normalizes:

java-oop
JAVA-OOP
 Java-Oop

to:

JAVA-OOP

Therefore equality becomes simple:

value.equals(
        other.value
);

You do not need to repeatedly perform:

equalsIgnoreCase()

throughout the application।


equals() Contract

A correct equals() implementation must follow several rules।

These rules are known as the:

equals contract

Main properties:

Reflexive
Symmetric
Transitive
Consistent
Non-null

Reflexive

For any non-null reference:

x.equals(
        x
)

must be:

true

Example:

CourseCode code =
        new CourseCode(
                "JAVA"
        );

code.equals(
        code
);

must return:

true

Symmetric

If:

a.equals(
        b
)

is true, then:

b.equals(
        a
)

must also be true।

You cannot have:

A says equal to B
B says not equal to A

Transitive

If:

A equals B
B equals C

then:

A must equal C

Example:

a.equals(
        b
);

b.equals(
        c
);

If both are true:

a.equals(
        c
);

must also be true।


Consistent

If no equality-related state changes:

a.equals(
        b
)

should keep returning the same result।

It should not randomly depend on:

Current time
Random number
Network response
Database state

Non-Null Rule

For any non-null object:

x.equals(
        null
)

must return:

false

It should not throw just because other is null।


Broken Equality Example

Bad:

@Override
public boolean equals(
        Object other
) {
    return Math.random()
            > 0.5;
}

This violates consistency and almost every useful equality expectation।


What Is hashCode()?

hashCode() returns an integer representing an object for hash-based data structures।

Example:

int hash =
        code.hashCode();

Hash-based collections include:

HashSet
HashMap

hashCode() Does Not Need to Be Unique

Two different objects can have the same hash code।

This is called:

hash collision

Example:

Object A → 12345
Object B → 12345

This is allowed।

Hash collections use equality checks to resolve collisions।


Critical equals() / hashCode() Rule

If:

a.equals(
        b
)

returns:

true

then:

a.hashCode()

and:

b.hashCode()

must return the same integer।

This is mandatory।


Reverse Is Not Required

If:

a.hashCode()
==
b.hashCode()

it does not mean:

a.equals(
        b
)

must be true।

Hash collisions are allowed।


Why hashCode() Matters

Suppose:

Set<CourseCode> codes =
        new HashSet<>();

Add:

codes.add(
        new CourseCode(
                "JAVA-OOP"
        )
);

Then:

codes.contains(
        new CourseCode(
                "JAVA-OOP"
        )
);

should return:

true

for value-based equality।

For this to work correctly, equals() and hashCode() must agree।


What HashSet Conceptually Does

Simplified:

Calculate hashCode
↓
Find likely bucket
↓
Use equals() among matching candidates

It does not usually scan every object from the beginning।


Broken hashCode()

Suppose:

@Override
public boolean equals(
        Object other
) {
    // value-based equality
}

but you do not override:

hashCode()

Then equal objects may have different default identity hashes।

A HashSet can behave incorrectly from the application's perspective।


Example Broken Class

public final class CourseCode {

    private final String value;

    @Override
    public boolean equals(
            Object other
    ) {
        if (
                !(other
                instanceof CourseCode courseCode)
        ) {
            return false;
        }

        return value.equals(
                courseCode.value
        );
    }

    // hashCode() missing
}

This is incomplete design।

General rule:

If you override equals(), almost always override hashCode() together.


Objects.hash()

Convenient implementation:

@Override
public int hashCode() {
    return Objects.hash(
            value
    );
}

For multiple fields:

return Objects.hash(
        firstName,
        lastName,
        dateOfBirth
);

Direct Field Hashing

For a single non-null field, you could also write:

@Override
public int hashCode() {
    return value.hashCode();
}

This is perfectly valid।

Objects.hash() is convenient but not mandatory।


Equality Based on Multiple Fields

Example:

public final class Money {

    private final long amountInCents;
    private final String currency;
}

Logical equality may require both:

amount
currency

because:

100 EUR

is not equal to:

100 USD

Money.equals()

@Override
public boolean equals(
        Object other
) {
    if (this == other) {
        return true;
    }

    if (
            !(other
            instanceof Money money)
    ) {
        return false;
    }

    return amountInCents
            == money.amountInCents
            && currency.equals(
                    money.currency
            );
}

Money.hashCode()

@Override
public int hashCode() {
    return Objects.hash(
            amountInCents,
            currency
    );
}

The same fields used in equality should normally contribute to hash code।


Objects.equals()

Suppose a field may be nullable:

private final String description;

Instead of:

description.equals(
        other.description
)

which fails if description == null, use:

Objects.equals(
        description,
        other.description
)

Behavior:

null, null       → true
null, "Java"     → false
"Java", null     → false
"Java", "Java"   → true

Prefer Non-Null Invariants When Appropriate

Objects.equals() is useful, but do not make every field nullable unnecessarily।

If:

Course title must exist

validate in constructor and keep:

title

non-null।

Then equality code becomes simpler।


Equality and Collections

Many collection methods depend on equals()

Examples:

list.contains(
        value
);

list.remove(
        value
);

set.contains(
        value
);

map.containsKey(
        key
);

So equality design has practical consequences throughout the application।


Example with List.contains()

List<CourseCode> codes =
        List.of(
                new CourseCode(
                        "JAVA"
                )
        );

boolean exists =
        codes.contains(
                new CourseCode(
                        "JAVA"
                )
        );

If CourseCode.equals() is value-based:

true

Without it:

false

for separate instances।


Duplicate Detection with Set

Set<CourseCode> codes =
        new HashSet<>();

codes.add(
        new CourseCode(
                "JAVA"
        )
);

codes.add(
        new CourseCode(
                "java"
        )
);

Because the constructor normalizes and equality is value-based, the set should contain only one logical value।


HashMap Keys

Example:

Map<CourseCode, Course> courses =
        new HashMap<>();

Insert:

courses.put(
        new CourseCode(
                "JAVA"
        ),
        course
);

Lookup:

Course loaded =
        courses.get(
                new CourseCode(
                        "java"
                )
        );

Correct equality/hashing makes this work।


Mutable Hash Keys Are Dangerous

Suppose:

public class MutableCourseCode {

    private String value;

    public void setValue(
            String value
    ) {
        this.value =
                value;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        ...
    }

    @Override
    public int hashCode() {
        return value.hashCode();
    }
}

Add as key:

MutableCourseCode code =
        new MutableCourseCode(
                "JAVA"
        );

map.put(
        code,
        course
);

Then change:

code.setValue(
        "BACKEND"
);

Now hashCode() changed while the key is already inside the map।

Lookup behavior may break।


Why the Map Breaks

At insertion:

hash("JAVA")
→ bucket A

After mutation:

hash("BACKEND")
→ bucket B

But the object is physically still stored in:

bucket A

The map searches based on the new hash and may look in:

bucket B

Result:

Key appears to be missing

Stable Equality Fields

A strong rule for hash keys:

Fields used by equals()/hashCode() should remain stable while the object is in a hash-based collection.

Immutable value objects naturally satisfy this।


Entity Equality Is More Complicated

For value objects:

Compare all meaningful value fields

is usually straightforward।

Entities are different।

Example:

Course

may have:

id
title
price
status
lessons

Does changing the title mean it becomes a different course?

Usually:

No

The entity retains identity while state changes।


Entity Identity

For an entity, equality might be based on:

Stable identifier

Example:

courseId

rather than all mutable fields।

But entity equality design depends on lifecycle and persistence rules।

There is no universal one-line rule।


Dangerous Entity Equality

Suppose Course.equals() uses:

title
status
lessons

Then publishing the course changes its equality/hash code।

If it is inside a:

HashSet<Course>

you can create the same mutable-key problem।


For This Course

Use a simple principle:

Value object → value-based equality
Mutable entity → be careful; prefer stable identity if equality is needed

Do not automatically generate equals() using every field of every entity।


Database ID and Equality

Suppose an entity receives database ID only after persistence:

Before save → id = null
After save  → id = 42

If equality/hash code depend entirely on id, the object's hash changes after save।

This requires careful design in ORM-heavy applications।

We will not solve every persistence-specific equality issue in this foundation course।

The key lesson:

Entity equality is a design decision, not an IDE checkbox.

Inheritance and Equality

Equality becomes harder with inheritance।

Suppose:

class Course

and:

class PremiumCourse
        extends Course

Should:

Course(code=JAVA)

equal:

PremiumCourse(code=JAVA)

?

Different answers can break symmetry or transitivity।

This is one reason immutable value classes are often declared:

final

It simplifies equality semantics।


getClass() vs instanceof

Some implementations use:

if (
        other == null
        || getClass()
        != other.getClass()
) {
    return false;
}

Others use:

instanceof

For final value classes, either can work cleanly because there are no subclasses।

Example:

public final class CourseCode

makes inheritance concerns disappear।


Do Not Compare Strings with ==

Classic mistake:

String first =
        new String(
                "Java"
        );

String second =
        new String(
                "Java"
        );

System.out.println(
        first == second
);

Likely:

false

because they are different objects।

Use:

first.equals(
        second
);

Result:

true

String Pool Can Mislead Beginners

Sometimes:

String first =
        "Java";

String second =
        "Java";

System.out.println(
        first == second
);

may print:

true

due to string interning।

Do not use this as a reason to compare strings with ==

Logical string comparison should use:

equals()

Primitive == Is Different

For primitives:

int a =
        10;

int b =
        10;

a == b

compares values directly।

Object references:

objectA == objectB

compare reference identity।


Enums and ==

Enums are special।

For enum constants:

status
==
CourseStatus.PUBLISHED

is correct and preferred।

Enum constants are singleton instances managed by Java।


toString()

Every object also inherits:

toString()

from:

Object

Default output may look like:

io.liveklass.course.CourseCode@4e50df2e

Not very useful।


Useful toString()

For a value object:

@Override
public String toString() {
    return value;
}

Then:

System.out.println(
        courseCode
);

prints:

JAVA-OOP

toString() for Composite Objects

Example:

@Override
public String toString() {
    return "Course{"
            + "code="
            + code
            + ", title='"
            + title
            + '\''
            + ", status="
            + status
            + '}';
}

Possible output:

Course{code=JAVA-OOP, title='Java Foundation', status=DRAFT}

Useful for debugging।


Do Not Put Secrets in toString()

Dangerous:

@Override
public String toString() {
    return "User{"
            + "password="
            + password
            + ", token="
            + token
            + '}';
}

Objects can be logged accidentally।

Avoid exposing:

Passwords
API keys
Access tokens
Private secrets
Sensitive personal data

through toString()


toString() Is Not Serialization

Do not use:

course.toString()

as a persistence format unless that is an explicitly designed contract।

toString() is usually intended for:

Debugging
Logging
Developer readability

Its format may change।


Equality and toString() Are Separate

Two equal objects do not need identical toString() by language contract, although they often naturally will if based on the same values।

toString() is not part of equality calculation unless you deliberately make the mistake of comparing string representations।

Bad:

return this.toString()
        .equals(
                other.toString()
        );

Do not implement equality that way।


IDE-Generated Equality

IDEs can generate:

equals()
hashCode()
toString()

This is useful।

But you still need to decide:

Which fields define equality?
Which fields must remain stable?
Is this a value object or entity?

Generation cannot make the design decision for you।


Example: Immutable Money

package io.liveklass.shared;

import java.util.Locale;
import java.util.Objects;

public final class Money {

    private final long amountInCents;
    private final String currency;

    public Money(
            long amountInCents,
            String currency
    ) {
        if (amountInCents < 0) {
            throw new IllegalArgumentException(
                    "Amount cannot be negative."
            );
        }

        if (
                currency == null
                || currency.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Currency is required."
            );
        }

        this.amountInCents =
                amountInCents;

        this.currency =
                currency.strip()
                        .toUpperCase(
                                Locale.ROOT
                        );
    }

    public long getAmountInCents() {
        return amountInCents;
    }

    public String getCurrency() {
        return currency;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                !(other
                instanceof Money money)
        ) {
            return false;
        }

        return amountInCents
                == money.amountInCents
                && currency.equals(
                        money.currency
                );
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                amountInCents,
                currency
        );
    }

    @Override
    public String toString() {
        return amountInCents
                + " "
                + currency;
    }
}

Equality Test Example

Money first =
        new Money(
                1_000L,
                "eur"
        );

Money second =
        new Money(
                1_000L,
                "EUR"
        );

System.out.println(
        first.equals(
                second
        )
);

System.out.println(
        first.hashCode()
        ==
        second.hashCode()
);

Output:

true
true

because currency is normalized।


Complete Example with HashSet

Set<Money> prices =
        new HashSet<>();

prices.add(
        new Money(
                1_000L,
                "EUR"
        )
);

prices.add(
        new Money(
                1_000L,
                "eur"
        )
);

System.out.println(
        prices.size()
);

Output:

1

Both objects represent the same logical value।


Complete Example with HashMap

Map<CourseCode, String> titles =
        new HashMap<>();

titles.put(
        new CourseCode(
                "JAVA-OOP"
        ),
        "Java and OOP Foundation"
);

String title =
        titles.get(
                new CourseCode(
                        "java-oop"
                )
);

System.out.println(
        title
);

Output:

Java and OOP Foundation

Correct equality and hashing make this possible।


Common Mistakes

Using == for Logical Object Equality

Checks identity, not value equality।


Using == for Strings

May appear to work due to string interning, then fail elsewhere।


Overriding equals() Without hashCode()

Breaks hash-based collection expectations।


Using Different Fields in equals() and hashCode()

Can violate their contract।


Including Mutable Fields in Hash-Based Identity

Mutation can make keys or set members effectively unreachable।


Assuming Hash Codes Are Unique

Collisions are allowed।


Using Every Entity Field in Equality Automatically

Mutable lifecycle fields may make equality unstable।


Using toString() as Equality

String representation is not a logical equality contract।


Logging Secrets Through toString()

Can create security problems।


Practice Exercises

Exercise 1: Implement CourseCode

Create immutable:

CourseCode

Requirements:

  • Normalize to uppercase
  • Override equals()
  • Override hashCode()
  • Override toString()

Verify:

new CourseCode(
        "java"
)

equals:

new CourseCode(
        "JAVA"
)

Exercise 2: Create LessonId

Create:

LessonId

wrapping a positive long

Implement value equality and hashing।


Exercise 3: Money Equality

Create:

Money(
        amountInCents,
        currency
)

Verify:

1000 EUR == 1000 eur
1000 EUR != 1000 USD
2000 EUR != 1000 EUR

Exercise 4: HashSet Duplicate Detection

Add these to a HashSet<CourseCode>:

JAVA
java
 Java
BACKEND

Predict final set size if normalization is correct।


Exercise 5: Find the Bug

public final class Email {

    private final String value;

    @Override
    public boolean equals(
            Object other
    ) {
        if (
                !(other
                instanceof Email email)
        ) {
            return false;
        }

        return value.equals(
                email.value
        );
    }
}

What is missing?


Exercise 6: Mutable Key Problem

Create a mutable class whose hashCode() depends on a mutable field।

Insert it into a HashMap, mutate the field, then attempt lookup।

Explain the result।


Predict the Result

Question 1

String first =
        new String(
                "Java"
        );

String second =
        new String(
                "Java"
        );

System.out.println(
        first == second
);

Answer

false

They are different object instances।


Question 2

System.out.println(
        first.equals(
                second
        )
);

Answer

true

Their string values are equal।


Question 3

CourseCode first =
        new CourseCode(
                "JAVA"
        );

CourseCode second =
        first;

System.out.println(
        first == second
);

Answer

true

Both variables reference the same object।


Question 4

If:

a.equals(
        b
)

is true, must:

a.hashCode()
==
b.hashCode()

also be true?

Answer

Yes।


Question 5

If two objects have the same hashCode(), must they be equal?

Answer

No।

Hash collisions are allowed।


True or False

  1. == and equals() always mean the same thing.
  2. Default Object.equals() is identity-based.
  3. Equal objects must have equal hash codes.
  4. Equal hash codes guarantee equal objects.
  5. HashMap depends on hashCode() and equals().
  6. Immutable objects are good hash keys.
  7. Mutable equality fields can cause hash collection bugs.
  8. Enum constants should usually be compared with ==.
  9. Strings should normally be compared with ==.
  10. toString() is a stable serialization contract.

Answers

1. False
2. True
3. True
4. False
5. True
6. True
7. True
8. True
9. False
10. False

Knowledge Check

Question 1

What does == compare for objects?

Question 2

What does equals() usually represent when overridden?

Question 3

What is value equality?

Question 4

What are the main rules of the equals() contract?

Question 5

What is the most important equals() / hashCode() relationship?

Question 6

Can different objects have the same hash code?

Question 7

Why do HashSet and HashMap need hashCode()?

Question 8

Why should equal objects use the same equality-related fields in hashCode()?

Question 9

Why are immutable value objects good map keys?

Question 10

What happens when a hash key's equality fields mutate?

Question 11

Should all fields of a mutable entity automatically participate in equality?

Question 12

When is Objects.equals() useful?

Question 13

What is toString() mainly for?

Question 14

Why should secrets not appear in toString()?

Question 15

Why should IDE-generated equality code still be reviewed?


Knowledge Check Answers

Answer 1

Whether both references point to the same object।

Answer 2

Logical equality defined by the class।

Answer 3

Equality based on the represented value rather than object identity।

Answer 4

Reflexivity, symmetry, transitivity, consistency, and returning false for null।

Answer 5

If two objects are equal, they must return the same hash code।

Answer 6

Yes. Hash collisions are valid।

Answer 7

They use hash codes to narrow down where values are stored or searched, then use equality for final matching।

Answer 8

Otherwise equal objects may produce incompatible hashes and break hash collection behavior।

Answer 9

Their equality and hash-related state remains stable after insertion।

Answer 10

The object's hash may change while it remains stored in its old bucket, making lookups unreliable।

Answer 11

No. Equality for entities should usually be based on stable identity semantics rather than blindly using mutable state।

Answer 12

When nullable references need null-safe equality comparison।

Answer 13

Developer-readable debugging and logging representation।

Answer 14

Because objects may be logged automatically and expose sensitive information।

Answer 15

The IDE can generate syntax, but only the developer can decide which fields actually define logical identity।


Lesson Summary

এই lesson-এ আমরা শিখেছি:

  • Object == reference identity compare করে
  • equals() logical equality define করতে পারে
  • Separate objects একই logical value represent করতে পারে
  • Value objects value-based equality-এর strong candidates
  • equals() must be reflexive, symmetric, transitive, consistent, and null-safe
  • Equal objects must produce the same hashCode()
  • Same hash code does not guarantee equality
  • HashSet এবং HashMap both hashing and equality-এর ওপর নির্ভর করে
  • Overriding equals() without hashCode() is a common bug
  • Equality-related fields should remain stable in hash collections
  • Immutable value objects excellent Map keys এবং Set elements
  • Mutable entity equality requires more careful identity decisions
  • Do not automatically include every mutable entity field in equality
  • Strings should use equals(), not ==
  • Enums can safely use ==
  • Objects.equals() null-safe comparisons support করে
  • Objects.hash() hash implementation simplify করতে পারে
  • toString() debugging এবং logging-এর জন্য useful
  • toString() persistence contract নয়
  • Sensitive information toString()-এ expose করা উচিত নয়
  • IDE-generated methods still require design judgment

Next Lesson

পরবর্তী lesson:

Records and Modern Data Models

আমরা শিখব:

  • record কী
  • Record components
  • Generated constructor
  • Generated accessors
  • Generated equals(), hashCode(), and toString()
  • Compact constructors
  • Validation inside records
  • Records as immutable data carriers
  • Record vs normal class
  • Record vs mutable entity
  • When records improve code and when they do not