Object Class Methods


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)

class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } } public class TestDefault { public static void main(String[] args) { Person p1 = new Person("Siraj", 25); Person p2 = new Person("Siraj", 25); System.out.println(p1.equals(p2));   // false (default == check) System.out.println(p1.hashCode()); // unique hash based on memory address System.out.println(p1.toString());     // Person@5a07e868 } }
Notice: without overriding, these methods don’t compare values (like name/age), they just use memory references. 


1. equals(Object obj)

Default behavior: Compares memory references (==).
When overridden: Compares object contents (values).

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)

class Person { String name; int age; Person(String name, int age) { this.name = name; this.age = age; } } public class Test1 { public static void main(String[] args) { Person p1 = new Person("Siraj", 25); Person p2 = new Person("Siraj", 25); System.out.println(p1 == p2);    // false (different objects in memory) System.out.println(p1.equals(p2)); // false (default == check) } }

Example 2: Overriding equals() with Multiple Fields

import java.util.Objects; class Employee { String id; String department; int age; Employee(String id, String department, int age) { this.id = id; this.department = department; this.age = age; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Employee)) return false; Employee e = (Employee) obj; return age == e.age && id.equals(e.id) && department.equals(e.department); } } public class Test2 { public static void main(String[] args) { Employee e1 = new Employee("E101", "IT", 30); Employee e2 = new Employee("E101", "IT", 30); Employee e3 = new Employee("E102", "HR", 28); System.out.println(e1.equals(e2)); // true (all fields match) System.out.println(e1.equals(e3)); // false } }

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. 

Note:
  • Always override hashCode() when overriding equals().
  • equals() must be:
    1. Reflexive: x.equals(x) → true
    2. Symmetric: if x.equals(y) → y.equals(x)
    3. Transitive: if x.equals(y) and y.equals(z) → x.equals(z)
    4. Consistent: multiple calls return same result unless values change
    5. Null-safety: x.equals(null) → false

2. hashCode()

Default behavior: Returns an integer based on memory address (not useful for comparisons).

When overridden: Works with equals() to help collections (HashMap, HashSet) store/retrieve objects correctly.

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)

import java.util.*; class BadBook { String title; BadBook(String title) { this.title = title; } @Override public int hashCode() { return 1; // BAD: forces all objects into same bucket } @Override public boolean equals(Object obj) { if (!(obj instanceof BadBook)) return false; return title.equals(((BadBook)obj).title); } @Override public String toString() { return title; } } public class BadHashDemo { public static void main(String[] args) { HashSet<BadBook> set = new HashSet<>(); set.add(new BadBook("Java")); set.add(new BadBook("Python")); set.add(new BadBook("C++")); System.out.println(set); // Output: [Java, Python, C++] (works but all in one bucket internally) } }

Example 2: Good hashCode() (Well-Distributed)

import java.util.*; class GoodBook { String title; String author; GoodBook(String title, String author) { this.title = title; this.author = author; } @Override public int hashCode() { return Objects.hash(title, author); } @Override public boolean equals(Object obj) { if (!(obj instanceof GoodBook)) return false; GoodBook b = (GoodBook) obj; return title.equals(b.title) && author.equals(b.author); } @Override public String toString() { return title + " by " + author; } } public class GoodHashDemo { public static void main(String[] args) { HashMap<GoodBook, String> map = new HashMap<>(); map.put(new GoodBook("Java", "James"), "Lang 1"); map.put(new GoodBook("Python", "Guido"), "Lang 2"); System.out.println(map); // Output: {Java by James=Lang 1, Python by Guido=Lang 2} } }

Hash Collision Explained

What is Collision?
When two different objects return the same hash code.

Example:

String s1 = "FB"; String s2 = "Ea"; System.out.println(s1.hashCode()); // 2236 System.out.println(s2.hashCode()); // 2236 System.out.println(s1.equals(s2)); // false

Both "FB" and "Ea" have the same hash code, but they are different objects.
This is a collision.


How Java Solves Collisions

  1. 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.

  2. 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

HashMap Buckets: Index 5[ "FB""Facebook" ] → [ "Ea""Earth" ] Index 12[ "Java""James" ] Index 17[ "Python""Guido" ]

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:

Bucket (Index 5): ("FB") / \ ("Ea") ("AB") / \ / \ ("CD") ("XY") ("PQ") ("ZZ")

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.


3. toString()

Default behavior: Returns "ClassName@HexHashCode".

When overridden: Provides a meaningful string representation.

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

class Person {} public class TestToString1 { public static void main(String[] args) { Person p = new Person(); System.out.println(p); // Output: Person@5a07e868 (default) } }

Example 2: With Override

class Customer { String id; String name; Customer(String id, String name) { this.id = id; this.name = name; } @Override public String toString() { return "Customer{id='" + id + "', name='" + name + "'}"; } } public class TestToString2 { public static void main(String[] args) { Customer c = new Customer("C101", "Siraj"); System.out.println(c); // Output: Customer{id='C101', name='Siraj'} } }

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.


4. clone()

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.


1. Shallow Copy


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.


2. Deep Copy


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()

  1. Gaming Apps
    • Saving and restoring player state.
    • Example: Player progress checkpoint.
  2. Word Processors / IDEs
    • Implementing undo/redo operations by cloning current document/code buffer.
  3. Simulation Systems
    • Rollback feature in simulations or AI-based models.
  4. Caching Systems
    • Duplicate cached objects safely without altering the original reference.
  5. Financial Apps
    • Cloning transactions/objects before risky calculations to preserve original state.


Rule of Thumb
  • 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. 


5. finalize() (Deprecated in Java9+)

Default behavior: Called by GC before destroying object (but unreliable).

Real use: Rare in modern Java (deprecated after Java 9).

Purpose
Called by GC before object is destroyed.

Default Behavior
Empty implementation.

Example (Old)

class FileHandler { @Override protected void finalize() throws Throwable { System.out.println("Closing file resource..."); } }

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).


6. getClass()

Default behavior: Returns runtime class info.

Use case: Reflection, frameworks like Hibernate, Spring.

Purpose
Returns runtime class of object.

Default Behavior
Always works, rarely overridden.

Example

class Customer {} public class TestGetClass { public static void main(String[] args) { Customer c = new Customer(); System.out.println(c.getClass().getName()); // Output: Customer } }

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. 

7. wait(), notify(), notifyAll()

Default behavior: Synchronization methods from Object.
Use case: Thread communication.

Purpose
Used for thread communication (producer-consumer problems).

Default Behavior
Must be called inside synchronized blocks.

Example: Producer-Consumer

import java.util.*; class Shared { private Queue<Integer> queue = new LinkedList<>(); private int LIMIT = 1; public synchronized void produce(int value) throws InterruptedException { while (queue.size() == LIMIT) wait(); queue.add(value); System.out.println("Produced: " + value); notifyAll(); } public synchronized int consume() throws InterruptedException { while (queue.isEmpty()) wait(); int val = queue.poll(); System.out.println("Consumed: " + val); notifyAll(); return val; } }

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.


Summary Table (Cheat Sheet)


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





Example

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())

class Employee { int id; String name; double salary; Employee(int id, String name, double salary) { this.id = id; this.name = name; this.salary = salary; } // equals() - logical equality based on id @Override public boolean equals(Object obj) { if (this == obj) return true; // same memory reference if (!(obj instanceof Employee)) return false; // check class type Employee other = (Employee) obj; return this.id == other.id; // compare by employee id } // hashCode() - must be consistent with equals() @Override public int hashCode() { return Integer.hashCode(id); } // toString() - for meaningful print instead of memory address @Override public String toString() { return "Employee{id=" + id + ", name='" + name + "', salary=" + salary + "}"; } }

🔹 Example 1: Without Stream API

import java.util.*; public class EmployeeDemoWithoutStream { public static void main(String[] args) { // Creating employees Employee e1 = new Employee(101, "Alice", 50000); Employee e2 = new Employee(102, "Bob", 60000); Employee e3 = new Employee(101, "Alice Duplicate", 50000); // same id as e1 // HashSet will use equals() + hashCode() to avoid duplicates Set<Employee> employeeSet = new HashSet<>(); employeeSet.add(e1); employeeSet.add(e2); employeeSet.add(e3); // Won’t be added because e1 and e3 are "equal" // Printing all employees using toString() System.out.println("Employees in Set:"); for (Employee emp : employeeSet) { System.out.println(emp); // calls toString() } // Search employee manually (without streams) Employee search = new Employee(102, "Bob Duplicate", 70000); boolean found = false; for (Employee emp : employeeSet) { if (emp.equals(search)) { // calls overridden equals() found = true; break; } } System.out.println("Is employee with id=102 found? " + found); } }

Output

Employees in Set: Employee{id=101, name='Alice', salary=50000.0} Employee{id=102, name='Bob', salary=60000.0} Is employee with id=102 found? true

🔹 Example 2: With Stream API

import java.util.*; import java.util.stream.*; public class EmployeeDemoWithStream { public static void main(String[] args) { // Creating employees List<Employee> employees = Arrays.asList( new Employee(201, "Charlie", 70000), new Employee(202, "David", 80000), new Employee(201, "Charlie Duplicate", 70000) // duplicate id ); // Remove duplicates using distinct() → needs equals() + hashCode() List<Employee> uniqueEmployees = employees.stream() .distinct() .collect(Collectors.toList()); System.out.println("Unique Employees:"); uniqueEmployees.forEach(System.out::println); // calls toString() // Search employee using Stream API Employee search = new Employee(202, "David Duplicate", 90000); boolean found = employees.stream() .anyMatch(emp -> emp.equals(search)); // calls equals() System.out.println("Is employee with id=202 found? " + found); } }

Output

Unique Employees: Employee{id=201, name='Charlie', salary=70000.0} Employee{id=202, name='David', salary=80000.0} Is employee with id=202 found? true

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 in Object class compares memory references (like ==).

  • But in real-world applications, we usually want to compare object content (logical equality).

👉 Example:

class Employee { String name; int id; Employee(String name, int id) { this.name = name; this.id = id; } } public class Test { public static void main(String[] args) { Employee e1 = new Employee("Siraj", 101); Employee e2 = new Employee("Siraj", 101); System.out.println(e1.equals(e2)); // false (compares references) } }

To fix this:

@Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof Employee)) return false; Employee e = (Employee) obj; return this.id == e.id && this.name.equals(e.name); }

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 same hashCode().

  • Used in Hash-based collections (e.g., HashMap, HashSet).

👉 Example:

HashSet<Employee> set = new HashSet<>(); set.add(new Employee("Siraj", 101)); set.add(new Employee("Siraj", 101)); System.out.println(set.size());
  • 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() in Object → returns ClassName@HexHashCode.
    Example:

Employee e = new Employee("Siraj", 101); System.out.println(e); // Output: Employee@1b6d3586
  • By overriding, we provide a readable representation of the object:

@Override public String toString() { return "Employee{name='" + name + "', id=" + id + "}"; }

Output: Employee{name='Siraj', id=101} → helpful for debugging/logging.


Q5. What are the contracts of equals() and hashCode()?

Answer:

  1. If two objects are equal (equals() true) → they must return the same hashCode().

  2. If two objects have same hashCode(), they may or may not be equal.

  3. equals() must be:

    • Reflexivex.equals(x) is true.

    • Symmetric → if x.equals(y) then y.equals(x).

    • Transitive → if x.equals(y) and y.equals(z) then x.equals(z).

    • Consistent → repeated calls must return same result if no change.

    • Non-nullx.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 different hashCode(), a HashSet may store them as two separate entries.
    👉 Violates the contract of HashMap/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:

Employee e1 = new Employee("Siraj", 101); Employee e2 = new Employee("Siraj", 101); System.out.println(e1.equals(e2)); // false (different references) System.out.println(e1.hashCode() == e2.hashCode()); // true
  • 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(), then equals() only if needed.


Q11. Can we override toString() only?

Answer:

  • Yes, it doesn’t affect equals() or hashCode().

  • 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:

String s1 = new String("Java"); String s2 = new String("Java"); System.out.println(s1 == s2); // false (different objects) System.out.println(s1.equals(s2)); // true (content match)

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, its hashCode() may change → object becomes “lost” in the collection.
    👉 That’s why String 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):

    1. Compute hashCode().

    2. Find bucket.

    3. 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 in HashMap.

  • toString(): Print customer details when logging transactions.