Every class in Java implicitly extends the Object class (unless it explicitly extends another class).
So methods like equals(), hashCode(), toString(), clone(), etc., are inherited by all classes.
Default behavior = how these methods behave if you don’t override them in your class.
Example (Using default methods of Object class)
Purpose
Compares the contents (logical equality) of two objects.
Used to check if two objects are “meaningfully” equal.
Default Behavior
In Object class, it behaves like ==
→ compares memory references.
Override Needed
To compare values (fields) instead of memory references.
Example 1: Without Override (Default Behavior)
Example 2: Overriding equals() with Multiple Fields
Real-World Use Cases of equals():
-
Banking Systems: prevent duplicate customer registrations based on unique fields (Aadhaar/SSN).
-
E-commerce: avoid adding duplicate product entries in shopping cart (same productId, size, color).
-
Authentication Systems: compare login input vs stored user credentials.
-
Employee Management System: ensure no duplicate employee exists before inserting/updating record.
- Always override hashCode() when overriding equals().
- equals() must be:
- Reflexive: x.equals(x) → true
- Symmetric: if x.equals(y) → y.equals(x)
- Transitive: if x.equals(y) and y.equals(z) → x.equals(z)
- Consistent: multiple calls return same result unless values change
- Null-safety: x.equals(null) → false
Default behavior: Returns an integer based on memory address (not useful for comparisons).
Purpose
Returns an integer hash value used in HashMap, HashSet, Hashtable.
Default Behavior
Based on memory address (unique per object).
Override Needed
To ensure objects that are equal (equals() == true
) have the same hash code.
Example 1: Bad hashCode() (Causes Collisions)
Example 2: Good hashCode() (Well-Distributed)
What is Collision?
When two different objects return the same hash code.
Example:
Both "FB"
and "Ea"
have the same hash code, but they are different objects.
This is a collision.
How Java Solves Collisions
-
Chaining (Linked List in bucket)
-
If two objects have the same hash code, they are stored in the same bucket.
-
They are linked together using a linked list.
-
During lookup, Java uses
equals()
to find the correct object.
-
-
Treeification (Java 8+)
-
If too many collisions occur in one bucket (list length > 8),
the linked list is converted into a balanced Red-Black Tree. -
This improves lookup time from O(n) to O(log n).
-
Memory Diagram: Collision with Chaining
Even though "FB"
and "Ea"
collide at index 5, both are stored in a linked list.
Lookup uses equals()
to differentiate them.
Memory Diagram: Treeification (Java 8+)
When too many collisions happen:
The linked list becomes a balanced tree for faster lookup.
Real-World Analogy
Think of HashMap like library shelves (buckets):
-
Each book (object) is placed based on a number (hash).
-
If two books have the same number, they share the same shelf (chaining).
-
If one shelf has too many books, the librarian arranges them in a tree so searching is faster.
Real-World Use Cases of hashCode()
-
HashMap Cache: storing employee details with ID as key.
-
HashSet: preventing duplicate products in shopping cart.
-
LRU Cache: ensures O(1) lookups.
-
ORM Frameworks (Hibernate): entity identity comparisons.
Default behavior: Returns "ClassName@HexHashCode".
Purpose
Returns string representation of an object. Useful in logging, debugging, REST responses.
Default Behavior
Returns ClassName@HexHashCode
.
Override Needed
To show meaningful info.
Example 1: Without Override
Example 2: With Override
Real-World Use Cases of toString():
-
Logging (Log4j, SLF4J): printing meaningful details.
-
REST APIs (Spring Boot): objects auto-converted to JSON.
-
Debugging in IDEs: watch object states quickly.
-
Monitoring: useful in alert messages.
Default behavior: Shallow copy
- Primitive fields (e.g., int, double, boolean) are copied by value → the clone gets its own independent copy of these values.
- Reference fields (e.g., objects, arrays, collections) are copied by reference → only the memory address is copied, so both the original object and the cloned object still point to the same referenced object, instead of creating a new one.
When overridden: Can implement deep copy
- A class can override clone() to perform a deep copy, where not only the primitive fields are copied, but also new, independent copies of referenced objects are created.
- This ensures the cloned object is fully independent of the original, so changes in nested objects of one do not affect the other.
Purpose
The clone() method creates and returns a copy of an object.
- Default: Shallow Copy
- To use it, the class must implement the Cloneable interface; otherwise, calling clone() throws CloneNotSupportedException.
Default Behavior
- In Object class, clone() performs a shallow copy (copies field values as-is).
- If the class does not implement Cloneable, clone() throws CloneNotSupportedException.
Definition:
- In a shallow copy, primitive fields (e.g., int, double, boolean) are copied by value → the clone gets its own independent copy of these values.
- Reference fields (e.g., objects, arrays, collections) are copied by reference → only the memory address is copied, so both the original object and the cloned object still point to the same referenced object.
- This means changes made to a referenced object in one instance are reflected in the other.
Example: Shallow Copy
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class Player implements Cloneable {
String name;
int score;
Address address; // reference type
Player(String name, int score, Address address) {
this.name = name;
this.score = score;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // performs shallow copy
}
}
public class ShallowCopyDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("Mumbai");
Player p1 = new Player("Siraj", 100, addr);
Player p2 = (Player) p1.clone(); // shallow copy
// Change original player's address
p1.address.city = "Delhi";
System.out.println("P1 Address: " + p1.address.city); // Delhi
System.out.println("P2 Address: " + p2.address.city); // Delhi (changed too!)
}
}
Explanation:
- Both p1 and p2 share the same Address object reference.
- Modifying one affects the other → problematic in real-world apps.
Definition:
- In a deep copy, both primitive fields and referenced objects are fully copied.
- For reference fields (e.g., objects, arrays, collections), a new independent copy is created instead of just copying the memory address.
- This makes the cloned object completely independent from the original — changes made to one object do not affect the other.
Example: Deep Copy
class Address implements Cloneable {
String city;
Address(String city) {
this.city = city;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return new Address(this.city); // new Address object created
}
}
class Player implements Cloneable {
String name;
int score;
Address address;
Player(String name, int score, Address address) {
this.name = name;
this.score = score;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Player cloned = (Player) super.clone();
cloned.address = (Address) address.clone(); // deep copy of reference
return cloned;
}
}
public class DeepCopyDemo {
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("Mumbai");
Player p1 = new Player("Siraj", 100, addr);
Player p2 = (Player) p1.clone(); // deep copy
// Change original player's address
p1.address.city = "Delhi";
System.out.println("P1 Address: " + p1.address.city); // Delhi
System.out.println("P2 Address: " + p2.address.city); // Mumbai (unchanged)
}
}
Explanation:
- p1 and p2 now have separate Address objects.
- Modifying one does not affect the other.
- This is the preferred way when mutable reference fields exist.
Memory Diagram (Conceptual)
Shallow Copy:
p1 ---> Player[name="Siraj", address -> Addr("Delhi")]
p2 ---> Player[name="Siraj", address -> Addr("Delhi")] // same object shared
Deep Copy:
p1 ---> Player[name="Siraj", address -> Addr("Delhi")]
p2 ---> Player[name="Siraj", address -> Addr("Mumbai")] // independent copy
Real-World Use Cases of clone()
- Gaming Apps
- Saving and restoring player state.
- Example: Player progress checkpoint.
- Word Processors / IDEs
- Implementing undo/redo operations by cloning current document/code buffer.
- Simulation Systems
- Rollback feature in simulations or AI-based models.
- Caching Systems
- Duplicate cached objects safely without altering the original reference.
- Financial Apps
- Cloning transactions/objects before risky calculations to preserve original state.
-
Shallow Copy → Use when the object only has primitives or immutable objects (like String, LocalDate, UUID), or when it’s okay for the original and copy to share the same references.
-
Deep Copy → Use when the object has mutable objects (arrays, lists, other custom objects) and you want the copy to be fully independent from the original.
Default behavior: Called by GC before destroying object (but unreliable).
Purpose
Called by GC before object is destroyed.
Default Behavior
Empty implementation.
Example (Old)
Real-World Use Cases of finalize():
-
Used in older Java for cleaning file handles, sockets, DB connections.
-
Rare in modern apps (replaced by try-with-resources).
Default behavior: Returns runtime class info.
Purpose
Returns runtime class of object.
Default Behavior
Always works, rarely overridden.
Example
Real-World Use Cases of getClass():
-
Spring & Hibernate Reflection: inspect bean/entity class dynamically.
-
Testing (JUnit): get class under test.
-
Generic Libraries: logging type information.
Object
.Purpose
Used for thread communication (producer-consumer problems).
Default Behavior
Must be called inside synchronized blocks.
Example: Producer-Consumer
Real-World Use Cases of wait/notify:
-
Messaging Queues (RabbitMQ, Kafka): producer-consumer model.
-
Job Schedulers: worker threads wait until task is available.
-
Thread Pools: idle workers wait until notified.
Method | Default Behavior | When to Override | Real-World Use Case |
equals() | Reference compare (==) | Compare object contents | Avoiding duplicate customers in banking app |
hashCode() | Memory-based hash | Work with equals in hash collections | Storing customers in HashSet |
toString() | ClassName@Hex | Return meaningful info | Debugging/logging in REST APIs |
clone() | Shallow copy | Deep copy or controlled cloning | Save game / undo feature |
finalize() | Empty | (Deprecated) Cleanup resources | Old resource management |
getClass() | Returns runtime class | Rarely overridden | Reflection in Spring/Hibernate |
wait()/notify() | Thread communication | Not overridden | Producer-consumer problem |
Here is real-time example showing how equals()
, hashCode()
, and toString()
are used. It cover both versions:
-
Without Stream API (classic Java loop style)
-
With Stream API (modern, concise way)
🔹 Employee Class (with overridden methods -- equals(), hashCode(), toString())
🔹 Example 1: Without Stream API
Output
🔹 Example 2: With Stream API
Output
Key Takeaways
-
equals() → Defines logical equality (e.g., Employee equality by
id
). -
hashCode() → Ensures objects that are equal produce the same hash (important in HashSet, HashMap).
-
toString() → Provides readable object info instead of memory reference.
-
Without Stream API → Classic loops, more verbose.
-
With Stream API → Concise, modern, functional style.
Rule of Thumb
-
If you are storing custom objects in HashSet, HashMap, or using distinct() in Streams → always override
equals()
+hashCode()
. -
If not overridden → duplicates (logically equal objects) will not be detected.
Q&A
Q1. Why do we need to override equals()
in Java?
Answer:
-
The default
equals()
method inObject
class compares memory references (like==
). -
But in real-world applications, we usually want to compare object content (logical equality).
👉 Example:
To fix this:
Now, e1.equals(e2)
→ true
.
Q2. Why do we need to override hashCode()
when we override equals()
?
Answer:
-
Contract: If two objects are equal (
equals()
→ true), they must have the samehashCode()
. -
Used in Hash-based collections (e.g.,
HashMap
,HashSet
).
👉 Example:
-
Without overriding
hashCode()
, output =2
(duplicates allowed). -
After overriding
hashCode()
properly → output =1
.
Both must be overridden together to maintain consistent behavior in collections.
Q3. What is the default implementation of hashCode()
in Object class?
Answer:
-
Returns an integer derived from the object’s memory address (not guaranteed to be unique, but usually distinct).
-
That’s why two different objects usually have different hash codes, even if their contents are same.
Q4. Why do we override toString()
method?
Answer:
-
Default
toString()
inObject
→ returnsClassName@HexHashCode
.
Example:
-
By overriding, we provide a readable representation of the object:
Output: Employee{name='Siraj', id=101}
→ helpful for debugging/logging.
Q5. What are the contracts of equals()
and hashCode()
?
Answer:
-
If two objects are equal (
equals()
true) → they must return the samehashCode()
. -
If two objects have same
hashCode()
, they may or may not be equal. -
equals()
must be:-
Reflexive →
x.equals(x)
is true. -
Symmetric → if
x.equals(y)
theny.equals(x)
. -
Transitive → if
x.equals(y)
andy.equals(z)
thenx.equals(z)
. -
Consistent → repeated calls must return same result if no change.
-
Non-null →
x.equals(null)
must return false.
-
Q6. Real-time use cases?
-
equals(): Compare objects logically → e.g., checking if two
Customer
objects represent the same person in a banking app. -
hashCode(): Used in collections like
HashMap
,HashSet
,HashTable
to find & store objects quickly. -
toString(): Used in logging, debugging, and displaying object details in UI.
Q7. What happens if we override equals()
but not hashCode()
?
Answer:
-
Collection behavior breaks.
-
Example: If two objects are equal (by
equals()
), but have differenthashCode()
, aHashSet
may store them as two separate entries.
👉 Violates the contract ofHashMap
/HashSet
.
Q8. What happens if we override hashCode()
but not equals()
?
Answer:
-
Two objects may end up with the same hash code but still not be equal.
-
Example:
-
This creates confusion in collections → both will be stored separately.
Q9. Can two different objects have the same hashCode()
?
Answer:
-
Yes, called hash collision.
-
Example: Different strings may produce same hash code due to limited integer range.
-
Hash-based collections handle collisions internally using bucket + chaining or tree structure.
Q10. Is equals()
faster or hashCode()
faster?
Answer:
-
hashCode()
is faster → O(1), just returns an integer. -
equals()
can be O(n) if many fields are compared. -
That’s why collections first check
hashCode()
, thenequals()
only if needed.
Q11. Can we override toString()
only?
Answer:
-
Yes, it doesn’t affect
equals()
orhashCode()
. -
Commonly done for debugging/logging to get meaningful string output.
Q12. What is the difference between ==
and equals()
?
Answer:
-
==
→ compares references (memory address). -
equals()
→ compares contents (if overridden).
👉 Example:
Q13. Can we make equals()
always return true?
Answer:
-
Technically yes, but it breaks contracts.
-
All objects become "equal", causing issues in collections (like
HashSet
will only keep 1 element).
Q14. What happens if hashCode()
always returns the same value?
Answer:
-
Allowed but very inefficient.
-
All objects go into the same bucket in
HashMap
→ degrading performance from O(1) → O(n).
Q15. Can we use mutable fields inside equals()
and hashCode()
?
Answer:
-
Bad practice.
-
If an object is stored in
HashSet
/HashMap
and its fields change, itshashCode()
may change → object becomes “lost” in the collection.
👉 That’s whyString
is immutable and safe for keys.
Q16. Why are equals()
and hashCode()
important for collections like HashSet
and HashMap
?
Answer:
-
They are used to check uniqueness and find objects quickly.
-
Process in
HashSet.add(obj)
:-
Compute
hashCode()
. -
Find bucket.
-
Use
equals()
to check for duplicates.
-
Q17. Real-time Example – Banking App
-
equals(): Check if two customers are the same based on
accountNumber
. -
hashCode(): Use
accountNumber
to quickly locate customer records inHashMap
. -
toString(): Print customer details when logging transactions.