Professional Java Practices
Equality, Hashing, and Object Contracts
আপনি একটি 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()contracthashCode()contractHashSetএবং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 overridehashCode()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
==andequals()always mean the same thing.- Default
Object.equals()is identity-based. - Equal objects must have equal hash codes.
- Equal hash codes guarantee equal objects.
HashMapdepends onhashCode()andequals().- Immutable objects are good hash keys.
- Mutable equality fields can cause hash collection bugs.
- Enum constants should usually be compared with
==. - Strings should normally be compared with
==. 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এবংHashMapboth hashing and equality-এর ওপর নির্ভর করে- Overriding
equals()withouthashCode()is a common bug - Equality-related fields should remain stable in hash collections
- Immutable value objects excellent
Mapkeys এবংSetelements - 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-এর জন্য usefultoString()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(), andtoString() - 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