Java Object Class
Java Object Class
Every class in Java — whether you write it, import it from a library, or use one from the standard library — silently extends java.lang.Object. You never declare this. The compiler adds it automatically whenever a class does not explicitly extend something else. It is the single root of Java's entire class hierarchy.
This matters practically because every object you ever create in Java carries eleven methods inherited from Object. Most developers use toString() and never think about the rest. Then they run into a bug where two objects that should be equal are not, or their objects disappear silently in a HashSet, or they cannot figure out why their wait() call throws IllegalMonitorStateException. Every one of those problems traces back to Object methods they did not understand.
The Object Class Hierarchy
java.lang.Object
|
+-- String
+-- Integer
+-- ArrayList
+-- YourCustomClass
+-- Every other Java class ever written
Every class implicitly extends Object when no other parent is declared.
Every Java object IS-A Object.
This single-root design is what makes polymorphic collections possible. A List<Object> can hold anything. Arrays.sort() can compare anything that implements Comparable. Reflection can inspect anything. It all works because everything shares one common ancestor.
All Methods of the Object Class
| Method | Return Type | Purpose |
|---|---|---|
toString() | String | Human-readable representation of the object |
equals(Object obj) | boolean | Logical equality comparison |
hashCode() | int | Integer hash code for use in hash-based collections |
getClass() | Class<?> | Runtime class of the object — cannot be overridden |
clone() | Object | Shallow field-by-field copy — requires Cloneable |
finalize() | void | Called by GC before object is collected — deprecated Java 9+ |
wait() | void | Causes thread to wait until notify() or notifyAll() |
wait(long timeout) | void | Wait with a timeout in milliseconds |
wait(long timeout, int nanos) | void | Wait with millisecond + nanosecond timeout |
notify() | void | Wakes one thread waiting on this object's monitor |
notifyAll() | void | Wakes all threads waiting on this object's monitor |
toString() — The Most Overridden Method
The default toString() implementation in Object returns ClassName@hexHashCode — a string that is technically accurate but completely useless for debugging. Every class that represents domain data should override it.
1// File: ToStringDemo.java
2
3public class ToStringDemo {
4
5 // Without override — gets the Object default
6 static class RawOrder {
7 String orderId;
8 double amount;
9 RawOrder(String id, double amt) { orderId = id; amount = amt; }
10 }
11
12 // With override — meaningful output
13 static class Order {
14 private final String orderId;
15 private final String customerId;
16 private final double amount;
17 private String status;
18
19 Order(String orderId, String customerId, double amount) {
20 this.orderId = orderId;
21 this.customerId = customerId;
22 this.amount = amount;
23 this.status = "PLACED";
24 }
25
26 public void setStatus(String status) { this.status = status; }
27
28 @Override
29 public String toString() {
30 return "Order{"
31 + "id='" + orderId + '\''
32 + ", customer='" + customerId + '\''
33 + ", amount=Rs." + amount
34 + ", status='" + status + '\''
35 + '}';
36 }
37 }
38
39 public static void main(String[] args) {
40
41 RawOrder raw = new RawOrder("ORD-001", 1499.0);
42 Order ord = new Order("ORD-002", "CUST-501", 2999.0);
43
44 // Default Object.toString() — class name + hex hash code
45 System.out.println("Default toString : " + raw);
46
47 // Overridden toString() — readable and useful
48 System.out.println("Overridden : " + ord);
49
50 // toString() is called implicitly in string concatenation and print statements
51 System.out.println("In concatenation : Order is " + ord);
52 }
53}Output:
Default toString : ToStringDemo$RawOrder@7852e922
Overridden : Order{id='ORD-002', customer='CUST-501', amount=Rs.2999.0, status='PLACED'}
In concatenation : Order is Order{id='ORD-002', customer='CUST-501', amount=Rs.2999.0, status='PLACED'}
toString() is called implicitly whenever an object is passed to System.out.println, used in string concatenation, or logged. A meaningful override makes debugging, logging, and error messages dramatically faster to read. A missing override means log files full of Order@1b6d3586 that tell you nothing.
equals() — Logical Equality
The default equals() in Object checks reference equality — it returns true only if both references point to the exact same object in memory. For domain objects where equality means "same data", this default is wrong.
1// File: EqualsDemo.java
2
3import java.util.Objects;
4
5public class EqualsDemo {
6
7 static class ProductWithoutEquals {
8 String sku;
9 String name;
10 ProductWithoutEquals(String sku, String name) {
11 this.sku = sku; this.name = name;
12 }
13 }
14
15 static class Product {
16 private final String sku;
17 private final String name;
18 private final double price;
19
20 Product(String sku, String name, double price) {
21 this.sku = sku;
22 this.name = name;
23 this.price = price;
24 }
25
26 @Override
27 public boolean equals(Object obj) {
28 if (this == obj) return true; // same reference
29 if (obj == null) return false; // null check
30 if (!(obj instanceof Product other)) return false; // type check + cast
31
32 // Two products are equal when they share the same SKU
33 return Objects.equals(this.sku, other.sku);
34 }
35
36 @Override
37 public int hashCode() {
38 return Objects.hash(sku); // must match equals — same field(s)
39 }
40
41 @Override
42 public String toString() {
43 return sku + " | " + name + " | Rs." + price;
44 }
45 }
46
47 public static void main(String[] args) {
48
49 ProductWithoutEquals p1 = new ProductWithoutEquals("SKU-101", "Headphones");
50 ProductWithoutEquals p2 = new ProductWithoutEquals("SKU-101", "Headphones");
51
52 System.out.println("Without equals override:");
53 System.out.println(" p1 == p2 : " + (p1 == p2)); // false — different refs
54 System.out.println(" p1.equals(p2) : " + p1.equals(p2)); // false — Object default
55
56 Product prod1 = new Product("SKU-201", "Wireless Mouse", 1299.0);
57 Product prod2 = new Product("SKU-201", "Wireless Mouse", 1299.0);
58 Product prod3 = new Product("SKU-202", "Keyboard", 2499.0);
59
60 System.out.println("\nWith equals override:");
61 System.out.println(" prod1.equals(prod2): " + prod1.equals(prod2)); // true — same SKU
62 System.out.println(" prod1.equals(prod3): " + prod1.equals(prod3)); // false — different SKU
63 System.out.println(" prod1.equals(null) : " + prod1.equals(null)); // false — null safe
64 }
65}Output:
Without equals override:
p1 == p2 : false
p1.equals(p2) : false
With equals override:
prod1.equals(prod2): true
prod1.equals(prod3): false
prod1.equals(null) : false
The five-step pattern inside equals() — same reference check, null check, type check with pattern matching, then field comparison — is not optional boilerplate. Skip the null check and equals(null) throws NullPointerException. Skip the reference check and it wastes time comparing an object to itself. Skip the type check and comparing a Product to a String throws ClassCastException.
hashCode() — The equals-hashCode Contract
hashCode() returns an integer used by HashMap, HashSet, and Hashtable to determine the bucket where an object is stored. The contract between equals() and hashCode() is rigid: if two objects are equal according to equals(), they must return the same hashCode(). Violating this contract breaks every hash-based collection in the Java standard library.
1// File: HashCodeContractDemo.java
2
3import java.util.*;
4
5public class HashCodeContractDemo {
6
7 // Breaks the contract — equals overridden but hashCode is not
8 static class BrokenUser {
9 String userId;
10 BrokenUser(String id) { this.userId = id; }
11
12 @Override
13 public boolean equals(Object obj) {
14 if (!(obj instanceof BrokenUser other)) return false;
15 return Objects.equals(this.userId, other.userId);
16 }
17 // hashCode NOT overridden — uses Object default (memory-based)
18 }
19
20 // Follows the contract — both overridden consistently
21 static class User {
22 private final String userId;
23 private final String email;
24
25 User(String userId, String email) {
26 this.userId = userId;
27 this.email = email;
28 }
29
30 @Override
31 public boolean equals(Object obj) {
32 if (this == obj) return true;
33 if (!(obj instanceof User other)) return false;
34 return Objects.equals(this.userId, other.userId);
35 }
36
37 @Override
38 public int hashCode() {
39 return Objects.hash(userId); // consistent with equals
40 }
41
42 @Override
43 public String toString() { return "User[" + userId + "]"; }
44 }
45
46 public static void main(String[] args) {
47
48 // Broken class — fails in HashSet
49 BrokenUser bu1 = new BrokenUser("USR-001");
50 BrokenUser bu2 = new BrokenUser("USR-001");
51
52 Set<BrokenUser> brokenSet = new HashSet<>();
53 brokenSet.add(bu1);
54
55 System.out.println("Broken contract:");
56 System.out.println(" bu1.equals(bu2) : " + bu1.equals(bu2)); // true — same logic
57 System.out.println(" Same hashCode? : " + (bu1.hashCode() == bu2.hashCode())); // false — broken
58 System.out.println(" Contains bu2? : " + brokenSet.contains(bu2)); // false — looks up wrong bucket
59
60 // Correct class — works in HashSet
61 User u1 = new User("USR-001", "priya@flipkart.com");
62 User u2 = new User("USR-001", "priya@flipkart.com");
63
64 Set<User> userSet = new HashSet<>();
65 userSet.add(u1);
66
67 System.out.println("\nCorrect contract:");
68 System.out.println(" u1.equals(u2) : " + u1.equals(u2)); // true
69 System.out.println(" Same hashCode? : " + (u1.hashCode() == u2.hashCode())); // true
70 System.out.println(" Contains u2? : " + userSet.contains(u2)); // true — correct bucket
71
72 Map<User, String> roleMap = new HashMap<>();
73 roleMap.put(u1, "ADMIN");
74 System.out.println(" Lookup with u2 : " + roleMap.get(u2)); // ADMIN — correct lookup
75 }
76}Output:
Broken contract:
bu1.equals(bu2) : true
Same hashCode? : false
Contains bu2? : false
Correct contract:
u1.equals(u2) : true
Same hashCode? : true
Contains bu2? : true
Lookup with u2 : ADMIN
The broken BrokenUser class passes the equality check but silently fails in every HashSet and HashMap. contains(bu2) returns false even though an equal object is in the set — because the hash code sends the lookup to a different bucket where it never finds anything. This is one of the most common bugs in fresher pull requests.
equals() vs == — Comparison Table
| Aspect | == operator | equals() method |
|---|---|---|
| Type | Operator | Method from Object |
| For primitives | Compares values directly | Not applicable — primitives have no methods |
| For objects | Compares memory addresses (references) | Compares logical equality — depends on override |
| Default behaviour for objects | Reference equality | Reference equality (same as == if not overridden) |
| Can be overridden | No — operator behaviour is fixed | Yes — every class can define its own equality |
| Null safe | null == null is true | obj.equals(null) — throws if obj is null |
Used by HashSet / HashMap | Never | Always — alongside hashCode() |
| String comparison | Compares references — usually wrong | Compares character content — correct |
| Best practice | Use for reference identity or primitives | Use for domain equality between objects |
getClass() — Runtime Type Information
getClass() returns the runtime Class object representing the actual type of the object. It cannot be overridden — it is a final method. The returned Class object is the gateway to reflection: you can inspect methods, fields, annotations, and interfaces without knowing the type at compile time.
1// File: GetClassDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class GetClassDemo {
7
8 static class Animal {}
9 static class Dog extends Animal {}
10
11 public static void main(String[] args) {
12
13 Animal animal = new Dog(); // reference: Animal, actual type: Dog
14 Dog dog = new Dog();
15 String text = "DevStackFlow";
16 List<String> list = new ArrayList<>();
17
18 // getClass() always returns the actual runtime type
19 System.out.println(animal.getClass().getName()); // full class name
20 System.out.println(animal.getClass().getSimpleName()); // short name
21 System.out.println(dog.getClass().getSimpleName());
22 System.out.println(text.getClass().getSimpleName());
23 System.out.println(list.getClass().getSimpleName());
24
25 System.out.println();
26
27 // getClass() vs instanceof for type checking
28 System.out.println("animal instanceof Animal : " + (animal instanceof Animal)); // true
29 System.out.println("animal instanceof Dog : " + (animal instanceof Dog)); // true
30 System.out.println("getClass == Animal.class : " + (animal.getClass() == Animal.class)); // false
31 System.out.println("getClass == Dog.class : " + (animal.getClass() == Dog.class)); // true
32
33 System.out.println();
34
35 // Practical use in a generic logger
36 logType(animal);
37 logType(text);
38 logType(list);
39 }
40
41 public static void logType(Object obj) {
42 System.out.println("[LOG] Processing object of type: "
43 + obj.getClass().getSimpleName());
44 }
45}Output:
GetClassDemo$Dog
Dog
Dog
String
ArrayList
animal instanceof Animal : true
animal instanceof Dog : true
getClass == Animal.class : false
getClass == Dog.class : true
[LOG] Processing object of type: Dog
[LOG] Processing object of type: String
[LOG] Processing object of type: ArrayList
instanceof traverses the inheritance chain — a Dog is also an Animal. getClass() returns only the exact runtime type. In equals() implementations where two objects must be exactly the same class to be considered equal, getClass() is the correct check. For general type compatibility, instanceof is preferred.
clone() — Shallow Copy
clone() from Object creates a new object with the same field values. The class must implement the Cloneable marker interface, otherwise clone() throws CloneNotSupportedException. The default implementation performs a shallow copy — primitive fields are copied by value, reference fields are copied by reference (pointing to the same objects).
1// File: CloneDemo.java
2
3import java.util.ArrayList;
4import java.util.List;
5
6public class CloneDemo {
7
8 static class ShoppingCart implements Cloneable {
9
10 private final String customerId;
11 private double discount;
12 private final List<String> items; // mutable reference field
13
14 ShoppingCart(String customerId) {
15 this.customerId = customerId;
16 this.discount = 0.0;
17 this.items = new ArrayList<>();
18 }
19
20 public void addItem(String item) { items.add(item); }
21 public void setDiscount(double d) { this.discount = d; }
22 public List<String> getItems() { return items; }
23 public double getDiscount() { return discount; }
24
25 // Shallow clone — items list is SHARED between original and clone
26 @Override
27 public ShoppingCart clone() {
28 try {
29 return (ShoppingCart) super.clone();
30 } catch (CloneNotSupportedException e) {
31 throw new AssertionError(e);
32 }
33 }
34
35 @Override
36 public String toString() {
37 return "Cart[" + customerId + "] discount=" + discount + " items=" + items;
38 }
39 }
40
41 public static void main(String[] args) {
42
43 ShoppingCart original = new ShoppingCart("CUST-101");
44 original.addItem("Laptop");
45 original.addItem("Mouse");
46 original.setDiscount(10.0);
47
48 ShoppingCart cloned = original.clone();
49
50 System.out.println("Before mutation:");
51 System.out.println(" Original: " + original);
52 System.out.println(" Clone : " + cloned);
53
54 // Mutating the primitive field — affects only the clone
55 cloned.setDiscount(20.0);
56
57 // Mutating the list — affects BOTH original and clone (shallow copy)
58 cloned.addItem("Keyboard");
59
60 System.out.println("\nAfter mutation:");
61 System.out.println(" Original: " + original); // list changed too
62 System.out.println(" Clone : " + cloned);
63
64 System.out.println("\nSame list object? "
65 + (original.getItems() == cloned.getItems())); // true — shared reference
66 }
67}Output:
Before mutation:
Original: Cart[CUST-101] discount=10.0 items=[Laptop, Mouse]
Clone : Cart[CUST-101] discount=10.0 items=[Laptop, Mouse]
After mutation:
Original: Cart[CUST-101] discount=10.0 items=[Laptop, Mouse, Keyboard]
Clone : Cart[CUST-101] discount=20.0 items=[Laptop, Mouse, Keyboard]
Same list object? true
The discount field is a primitive-backed double — changing it on the clone has no effect on the original. The items list is a reference — both objects point to the same ArrayList, so adding "Keyboard" to the clone's list modifies the shared list that both see. Deep cloning requires manually copying each mutable reference field.
wait(), notify(), notifyAll() — Thread Coordination
wait(), notify(), and notifyAll() are declared on Object — not on Thread — because Java's locking mechanism is per-object. Any object can serve as a monitor. A thread that calls wait() on an object releases that object's lock and sleeps. Another thread calling notify() on the same object wakes one waiting thread.
All three methods must be called from within a synchronized block on the same object. Calling them outside synchronized throws IllegalMonitorStateException.
1// File: WaitNotifyDemo.java
2
3public class WaitNotifyDemo {
4
5 static class OrderQueue {
6
7 private String pendingOrder = null;
8 private final Object lock = new Object();
9
10 public void placeOrder(String orderId) throws InterruptedException {
11 synchronized (lock) {
12 while (pendingOrder != null) {
13 System.out.println("[PRODUCER] Queue full — waiting...");
14 lock.wait(); // releases lock and waits
15 }
16 pendingOrder = orderId;
17 System.out.println("[PRODUCER] Order placed: " + orderId);
18 lock.notify(); // wakes the consumer thread
19 }
20 }
21
22 public String processOrder() throws InterruptedException {
23 synchronized (lock) {
24 while (pendingOrder == null) {
25 System.out.println("[CONSUMER] No order — waiting...");
26 lock.wait();
27 }
28 String order = pendingOrder;
29 pendingOrder = null;
30 System.out.println("[CONSUMER] Order processed: " + order);
31 lock.notify(); // wakes the producer if it was waiting
32 return order;
33 }
34 }
35 }
36
37 public static void main(String[] args) throws InterruptedException {
38
39 OrderQueue queue = new OrderQueue();
40
41 Thread producer = new Thread(() -> {
42 try {
43 queue.placeOrder("ORD-001");
44 Thread.sleep(100);
45 queue.placeOrder("ORD-002");
46 } catch (InterruptedException e) {
47 Thread.currentThread().interrupt();
48 }
49 });
50
51 Thread consumer = new Thread(() -> {
52 try {
53 Thread.sleep(50);
54 queue.processOrder();
55 Thread.sleep(100);
56 queue.processOrder();
57 } catch (InterruptedException e) {
58 Thread.currentThread().interrupt();
59 }
60 });
61
62 producer.start();
63 consumer.start();
64
65 producer.join();
66 consumer.join();
67 }
68}Output:
[PRODUCER] Order placed: ORD-001
[CONSUMER] Order processed: ORD-001
[PRODUCER] Order placed: ORD-002
[CONSUMER] Order processed: ORD-002
These low-level methods are the foundation of Java's built-in thread coordination. In production code, ExecutorService, BlockingQueue, CompletableFuture, and other high-level concurrency utilities handle this more safely. But the underlying mechanism is always wait() and notify() on an object monitor.
Real-World Example — Product Catalogue Domain Model
The Business Problem
An e-commerce platform like Meesho or Flipkart manages a product catalogue where products are deduplicated by SKU, stored in HashSet and HashMap for fast lookup, and logged throughout the system. Getting equals(), hashCode(), toString(), and getClass() right on the Product class is not optional — it determines whether the entire catalogue lookup system works correctly.
1// File: CatalogueProduct.java
2
3import java.util.Objects;
4
5public class CatalogueProduct {
6
7 private final String sku;
8 private final String name;
9 private final String category;
10 private double price;
11 private int stockQuantity;
12
13 public CatalogueProduct(String sku, String name,
14 String category, double price, int stockQuantity) {
15 Objects.requireNonNull(sku, "SKU cannot be null");
16 Objects.requireNonNull(name, "Name cannot be null");
17 Objects.requireNonNull(category, "Category cannot be null");
18
19 this.sku = sku;
20 this.name = name;
21 this.category = category;
22 this.price = price;
23 this.stockQuantity = stockQuantity;
24 }
25
26 public String getSku() { return sku; }
27 public String getName() { return name; }
28 public String getCategory() { return category; }
29 public double getPrice() { return price; }
30 public int getStockQuantity() { return stockQuantity; }
31
32 public void updatePrice(double newPrice) {
33 if (newPrice <= 0) throw new IllegalArgumentException("Price must be positive.");
34 this.price = newPrice;
35 }
36
37 public void adjustStock(int delta) {
38 if (stockQuantity + delta < 0) throw new IllegalArgumentException("Insufficient stock.");
39 this.stockQuantity += delta;
40 }
41
42 // Two products are the same catalogue entry if they share a SKU
43 @Override
44 public boolean equals(Object obj) {
45 if (this == obj) return true;
46 if (!(obj instanceof CatalogueProduct other)) return false;
47 return Objects.equals(sku, other.sku);
48 }
49
50 // Must match equals — same field(s)
51 @Override
52 public int hashCode() {
53 return Objects.hash(sku);
54 }
55
56 // Useful in logs, errors, and debugging output
57 @Override
58 public String toString() {
59 return "Product{"
60 + "sku='" + sku + '\''
61 + ", name='" + name + '\''
62 + ", category='" + category + '\''
63 + ", price=Rs." + price
64 + ", stock=" + stockQuantity
65 + '}';
66 }
67}1// File: ProductCatalogueDemo.java
2
3import java.util.*;
4
5public class ProductCatalogueDemo {
6
7 public static void main(String[] args) {
8
9 CatalogueProduct p1 = new CatalogueProduct("SKU-001", "Wireless Headphones", "Electronics", 2499.0, 150);
10 CatalogueProduct p2 = new CatalogueProduct("SKU-001", "Wireless Headphones", "Electronics", 2499.0, 150);
11 CatalogueProduct p3 = new CatalogueProduct("SKU-002", "Mechanical Keyboard", "Electronics", 4999.0, 80);
12 CatalogueProduct p4 = new CatalogueProduct("SKU-003", "Cotton Kurta", "Clothing", 899.0, 320);
13
14 System.out.println("=== toString() ===");
15 System.out.println(p1);
16 System.out.println(p3);
17
18 System.out.println("\n=== equals() and hashCode() ===");
19 System.out.println("p1.equals(p2) : " + p1.equals(p2)); // true — same SKU
20 System.out.println("p1.equals(p3) : " + p1.equals(p3)); // false
21 System.out.println("p1 hashCode : " + p1.hashCode());
22 System.out.println("p2 hashCode : " + p2.hashCode()); // same as p1
23
24 System.out.println("\n=== HashSet deduplication ===");
25 Set<CatalogueProduct> catalogue = new HashSet<>();
26 catalogue.add(p1);
27 catalogue.add(p2); // duplicate SKU — not added
28 catalogue.add(p3);
29 catalogue.add(p4);
30 System.out.println("Unique products in set: " + catalogue.size()); // 3
31
32 System.out.println("\n=== HashMap lookup ===");
33 Map<CatalogueProduct, Integer> reorderThresholds = new HashMap<>();
34 reorderThresholds.put(p1, 30);
35 reorderThresholds.put(p3, 15);
36
37 // Look up using p2 — different object, same SKU — should find p1's threshold
38 System.out.println("Reorder threshold via p2 : " + reorderThresholds.get(p2)); // 30
39
40 System.out.println("\n=== getClass() ===");
41 System.out.println("p1 class : " + p1.getClass().getSimpleName());
42 System.out.println("p1 superclass : " + p1.getClass().getSuperclass().getSimpleName());
43 System.out.println("Is CatalogueProduct? : " + (p1 instanceof CatalogueProduct));
44
45 System.out.println("\n=== Price update with toString() ===");
46 p1.updatePrice(2299.0);
47 System.out.println("Updated: " + p1);
48 }
49}Output:
=== toString() ===
Product{sku='SKU-001', name='Wireless Headphones', category='Electronics', price=Rs.2499.0, stock=150}
Product{sku='SKU-002', name='Mechanical Keyboard', category='Electronics', price=Rs.4999.0, stock=80}
=== equals() and hashCode() ===
p1.equals(p2) : true
p1.equals(p3) : false
p1 hashCode : 1586024434
p2 hashCode : 1586024434
=== HashSet deduplication ===
Unique products in set: 3
=== HashMap lookup ===
Reorder threshold via p2 : 30
=== Price update with toString() ===
Updated: Product{sku='SKU-001', name='Wireless Headphones', category='Electronics', price=Rs.2299.0, stock=150}
p1 and p2 are two separate objects representing the same catalogue entry. Because equals() and hashCode() are correctly overridden, the HashSet treats them as one entry (size remains 3), and the HashMap correctly retrieves p1's threshold when looking up with p2. Without these overrides, the catalogue would silently accumulate duplicates and all lookups would fail.
Best Practices
Override toString() in every domain class. Log files, error messages, and System.out.println calls all use toString() implicitly. A class without a meaningful toString() produces ClassName@hexcode in every log entry — useless for debugging. Make it a habit: any class that represents a domain concept gets a toString().
Always override hashCode() when you override equals(). The IDE can generate both. The risk of overriding one without the other is that HashSet.contains() and HashMap.get() silently return wrong results. Most static analysis tools will warn about an equals() without a corresponding hashCode() — listen to that warning.
Use Objects.equals() and Objects.hash() in your implementations. Objects.equals(a, b) handles null safely — no need to null-check each field before comparing. Objects.hash(field1, field2) computes a consistent combined hash without writing the prime-multiplication boilerplate manually.
Never call wait() or notify() in a loop without a condition check. Spurious wakeups — where a thread wakes up from wait() without notify() being called — are a documented JVM behaviour. Always wrap wait() in a while loop that re-checks the condition, not an if. The pattern while (!condition) { lock.wait(); } prevents acting on a spurious wakeup.
Common Mistakes
Mistake 1 — Overriding equals() Without hashCode()
1public class Session {
2 private String sessionId;
3
4 @Override
5 public boolean equals(Object obj) {
6 if (!(obj instanceof Session other)) return false;
7 return Objects.equals(sessionId, other.sessionId);
8 }
9 // hashCode() not overridden — uses Object.hashCode() (memory-based)
10 // Two equal Session objects return different hash codes
11 // HashSet<Session> will contain duplicates — silently
12}This compiles without error. The bug only surfaces at runtime when sessions appear duplicated in a HashSet or lookups in a HashMap return null for objects that are clearly present.
Mistake 2 — Using == to Compare Objects
1String a = new String("hello");
2String b = new String("hello");
3
4System.out.println(a == b); // false — different objects in memory
5System.out.println(a.equals(b)); // true — same character contentUsing == on Strings — and on any object — is almost always wrong unless you specifically need reference identity. String literals benefit from the String pool, so "hello" == "hello" sometimes returns true, which misleads beginners into thinking == works for content comparison. It does not. Use equals().
Mistake 3 — Calling wait() Without synchronized
1public class OrderProcessor {
2 private final Object lock = new Object();
3
4 public void waitForOrder() throws InterruptedException {
5 // Throws IllegalMonitorStateException — not inside synchronized
6 lock.wait();
7 }
8}Runtime exception: java.lang.IllegalMonitorStateException
wait(), notify(), and notifyAll() must be called inside a synchronized block on the same object they are called on. The thread must hold the object's monitor before it can release it via wait(). Calling outside synchronized always throws IllegalMonitorStateException.
Mistake 4 — Modifying Fields Used in equals() and hashCode() After Insertion
1Product p = new Product("SKU-001", "Headphones", 1999.0);
2Set<Product> set = new HashSet<>();
3set.add(p); // stored in bucket based on hash of "SKU-001"
4
5p.setSku("SKU-002"); // mutates the field used in hashCode()
6// The object is now in the wrong bucket — set.contains(p) returns false
7System.out.println(set.contains(p)); // false — even though p is in the setOnce an object is stored in a HashSet or as a key in a HashMap, mutating the fields that contribute to hashCode() corrupts the data structure. The object ends up in a bucket its current hash does not match. Either use immutable objects as keys, or use fields that never change — like IDs — in your equals() and hashCode().
Interview Questions
Q1. What is the Object class in Java and why does every class extend it?
java.lang.Object is the root of Java's class hierarchy. Every class implicitly extends it when no explicit parent is declared. This guarantees that every object in Java shares a common set of methods — toString(), equals(), hashCode(), getClass(), clone(), wait(), notify(), and notifyAll(). This single-root design makes polymorphic collections, reflection, and thread coordination possible without requiring explicit declarations in every class.
Q2. What is the contract between equals() and hashCode()?
If two objects are equal according to equals(), they must return the same value from hashCode(). The reverse is not required — two objects with the same hash code are not necessarily equal. Violating this contract breaks HashSet, HashMap, and Hashtable. An object stored in a HashSet with one hash code becomes unfindable if its fields change after insertion, because contains() looks in the bucket corresponding to the new hash.
Q3. What does the default equals() implementation in Object do?
The default equals() in Object compares references — it returns true only when both references point to the exact same object in memory. It is identical to the == operator for objects. For domain classes where equality means "same data", this default is almost always wrong and must be overridden to compare meaningful fields.
Q4. Why are wait(), notify(), and notifyAll() methods on Object and not on Thread?
Java's synchronisation mechanism is per-object, not per-thread. Any object can serve as a monitor — the lock is on the object, and threads compete for it. A thread waits on an object's monitor and releases that object's lock. If these methods were on Thread, there would be no way to associate the wait condition with a specific shared resource. Placing them on Object means any object can coordinate threads that share it.
Q5. What is the difference between == and equals() in Java?
== on objects compares memory addresses — it returns true only if both references point to the exact same object. equals() compares logical equality — the result depends on the override. For String, equals() compares character content. For a custom Product class, equals() might compare only the SKU. The key rule: always use equals() for content comparison between objects, and == only for reference identity or primitive comparison.
Q6. What is a shallow copy versus a deep copy in the context of Object.clone()?
Object.clone() produces a shallow copy — a new object with field values copied from the original. For primitive fields and immutable references like String, this works correctly. For mutable reference fields like List or Map, both the original and the clone point to the same underlying object, so mutations through either reference affect both. A deep copy requires explicitly creating new copies of every mutable referenced object, usually through a copy constructor or a custom clone() implementation that recursively copies all mutable fields.
FAQs
Can you instantiate the Object class directly?
Yes — new Object() is valid Java. Object is a concrete class, not abstract. However, a plain Object instance has almost no useful behaviour — its toString() returns a memory address, and equals() checks reference identity. In practice, creating a plain Object is used only as a lock object in concurrency code: private final Object lock = new Object().
What happens if you call toString() on a null reference?
Calling any method on null throws NullPointerException. However, String.valueOf(null) returns the string "null", and string concatenation "" + null also produces "null" without throwing. When logging objects that might be null, use String.valueOf(object) or Objects.toString(object, "default") for safe string conversion.
Is finalize() still used in modern Java?
No. finalize() was deprecated in Java 9 and marked for removal. It was called by the garbage collector before reclaiming an object, but the timing was unpredictable and the implementation was complex and fragile. Use try-with-resources and AutoCloseable for deterministic resource cleanup. java.lang.ref.Cleaner is the modern replacement for post-GC cleanup in rare cases where it is genuinely needed.
Why does toString() in Object return a hex number?
The default format is ClassName@hashCode where the hash code is displayed in hexadecimal. The hash code from the default Object.hashCode() is derived from the object's memory address (though not guaranteed to be the actual address after JVM optimisations). It is unique enough to distinguish objects but carries no domain meaning — which is exactly why every domain class should override it.
What does Objects.requireNonNull() do and how does it relate to Object class?
Objects.requireNonNull(value) is a utility method from java.util.Objects — not from java.lang.Object. It throws NullPointerException immediately if the value is null, with a clear message. Using it in constructors catches null arguments at creation time rather than when the null reference is finally dereferenced later in the code, which makes the exception appear at the root cause rather than at a symptom.
Summary
java.lang.Object is the invisible foundation every Java class builds on. Its eleven methods are available on every object you create — and three of them, equals(), hashCode(), and toString(), need to be overridden in almost every domain class you write.
The most consequential rule is the equals()-hashCode() contract. Overriding one without the other produces code that compiles cleanly and fails silently in every hash-based collection. The pattern is always the same: override both, base them on the same fields, and use Objects.equals() and Objects.hash() to handle null safely.
getClass() and instanceof answer related questions — getClass() for exact type identity, instanceof for type compatibility. clone() produces a shallow copy that surprises developers who expect a deep one. wait(), notify(), and notifyAll() power Java's built-in thread coordination and require a synchronized block to use correctly.
For interviews, know the equals()-hashCode() contract cold, be ready to write a correct equals() implementation from memory, explain why wait() lives on Object rather than Thread, and describe the difference between == and equals() with a concrete example.
What to Read Next
| Topic | Link |
|---|---|
| How equals() and hashCode() determine behaviour in HashMap and HashSet | Java HashMap → |
| How toString() appears throughout the Collections framework and debugging | Java Collections Framework → |
| How inheritance makes every class a subtype of Object automatically | Java Inheritance → |
| How polymorphism uses the Object type for universal method parameters | Java Polymorphism → |
| How encapsulation protects the fields that equals() and hashCode() depend on | Java Encapsulation → |