Java Tutorial
🔍

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

MethodReturn TypePurpose
toString()StringHuman-readable representation of the object
equals(Object obj)booleanLogical equality comparison
hashCode()intInteger hash code for use in hash-based collections
getClass()Class<?>Runtime class of the object — cannot be overridden
clone()ObjectShallow field-by-field copy — requires Cloneable
finalize()voidCalled by GC before object is collected — deprecated Java 9+
wait()voidCauses thread to wait until notify() or notifyAll()
wait(long timeout)voidWait with a timeout in milliseconds
wait(long timeout, int nanos)voidWait with millisecond + nanosecond timeout
notify()voidWakes one thread waiting on this object's monitor
notifyAll()voidWakes 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.

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

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

Java
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== operatorequals() method
TypeOperatorMethod from Object
For primitivesCompares values directlyNot applicable — primitives have no methods
For objectsCompares memory addresses (references)Compares logical equality — depends on override
Default behaviour for objectsReference equalityReference equality (same as == if not overridden)
Can be overriddenNo — operator behaviour is fixedYes — every class can define its own equality
Null safenull == null is trueobj.equals(null) — throws if obj is null
Used by HashSet / HashMapNeverAlways — alongside hashCode()
String comparisonCompares references — usually wrongCompares character content — correct
Best practiceUse for reference identity or primitivesUse 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.

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

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

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

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

Java
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

Java
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 content

Using == 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

Java
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

Java
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 set

Once 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

TopicLink
How equals() and hashCode() determine behaviour in HashMap and HashSetJava HashMap →
How toString() appears throughout the Collections framework and debuggingJava Collections Framework →
How inheritance makes every class a subtype of Object automaticallyJava Inheritance →
How polymorphism uses the Object type for universal method parametersJava Polymorphism →
How encapsulation protects the fields that equals() and hashCode() depend onJava Encapsulation →
Java Object Class | DevStackFlow