The Java Object Class in Depth
Every Java class silently extends java.lang.Object: the root of the entire class hierarchy. Object provides methods that every object inherits: toString(), equals(), hashCode(), getClass(), clone(), wait(), notify(), notifyAll(), and the deprecated finalize(). Understanding what each does, when to override it, and when not to is essential for writing correct, idiomatic Java.
Why Every Class Extends Object
Java has a single-root class hierarchy: every class, no matter how it is declared, implicitly extends java.lang.Objectclass Dogclass Dog extends ObjectObjectObjectObject
The guarantee extends beyond ordinary classes. Arrays are objects too: int[]String[]int[][]ObjectObjectObjectObject
The methods ObjectequalshashCodegetClassclonetoStringwaitnotifynotifyAllfinalize
toString(): Human-Readable Representation
toString()System.out.println()ObjectClassName@hexHashCodecom.example.Dog@7852e922
Override toString()toString()ClassName[field1=value1, field2=value2]
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Point{x=" + x + ", y=" + y + "}";
}
}
Point p = new Point(3, 7);
System.out.println(p); // Point{x=3, y=7}
System.out.println("Move to " + p); // Move to Point{x=3, y=7}
// With Records — toString() is generated automatically
record PointR(int x, int y) {}
System.out.println(new PointR(3, 7)); // PointR[x=3, y=7]
toString()equals() and hashCode(): Logical Equality
By default, equals()==PointMoneyEmployee
The equals-hashCode contract requires that if a.equals(b)truea.hashCode() == b.hashCode()HashMapHashSet
public class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Point other)) return false;
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Point a = new Point(3, 7);
Point b = new Point(3, 7);
System.out.println(a.equals(b)); // true — logical equality
System.out.println(a == b); // false — different instances
Set<Point> set = new HashSet<>();
set.add(a);
System.out.println(set.contains(b)); // true — hashCode contract upheld
getClass(): Runtime Type Information
getClass()Class<?>AnimalDoggetClass()Dog.classAnimal.classClass
A common use is checking exact type equality. Note that getClass() == other.getClass()instanceofClassequals()instanceofgetClass()
Object obj = "Hello";
System.out.println(obj.getClass()); // class java.lang.String
System.out.println(obj.getClass().getName()); // java.lang.String
System.out.println(obj.getClass().getSimpleName()); // String
// Runtime type vs compile-time type
Number n = Integer.valueOf(42);
System.out.println(n.getClass()); // class java.lang.Integer
System.out.println(n.getClass().getSuperclass()); // class java.lang.Number
// Checking exact type
Object a = new ArrayList<>();
Object b = new LinkedList<>();
System.out.println(a.getClass() == b.getClass()); // false
System.out.println(a instanceof List<?>); // true
System.out.println(b instanceof List<?>); // true
clone(): Copying Objects
The clone()Object
To make clone()Cloneableclone()publicCloneableObject.clone()CloneNotSupportedExceptionclone()clone()
// Option 1 — Cloneable (legacy, fragile)
public class IntList implements Cloneable {
private int[] data;
private int size;
public IntList(int[] data) {
this.data = data.clone(); // already a deep copy for primitive arrays
this.size = data.length;
}
@Override
public IntList clone() {
try {
IntList copy = (IntList) super.clone(); // shallow copy of fields
copy.data = this.data.clone(); // manually deep-copy the array
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // cannot happen — we implement Cloneable
}
}
}
// Option 2 — Copy constructor (preferred)
public class IntList2 {
private int[] data;
private int size;
public IntList2(IntList2 source) { // copy constructor
this.data = source.data.clone();
this.size = source.size;
}
}
clone()wait(), notify() and notifyAll(): Low-Level Thread Coordination
Every Java object has an intrinsic monitor: a built-in lock that the synchronizedwait()notify()notifyAll()java.util.concurrent
The rules are strict: all three must be called from within a synchronizedIllegalMonitorStateExceptionwait()notify()notifyAll()wait()while
import java.util.ArrayDeque;
import java.util.Queue;
public class MonitorQueue<T> {
private final Queue<T> queue = new ArrayDeque<>();
private final int capacity;
public MonitorQueue(int capacity) { this.capacity = capacity; }
public synchronized void put(T item) throws InterruptedException {
while (queue.size() == capacity) {
wait(); // release lock and suspend until space is available
}
queue.add(item);
notifyAll(); // wake consumers that may be waiting for items
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // release lock and suspend until an item is available
}
T item = queue.poll();
notifyAll(); // wake producers that may be waiting for space
return item;
}
}
java.util.concurrentwaitnotify- For producer-consumer queues, use
(e.g.,BlockingQueue ).LinkedBlockingQueue - For conditional waiting with explicit locks, use
andReentrantLock .Condition - For one-shot coordination, use
.CountDownLatch
finalize(): Why It Was Deprecated
finalize()finalize()finalize()this
finalize()
- try-with-resources: for any resource that implements
. The compiler guaranteesAutoCloseable is called even if an exception is thrown.close() - Cleaner API (
, Java 9+): for native or off-heap resources that genuinely cannot be wrapped injava.lang.ref.Cleaner . AAutoCloseable registers aCleaner that runs after the object becomes phantom-reachable, in a dedicated thread.Runnable
import java.lang.ref.Cleaner;
public class NativeBuffer implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
private final long nativePtr; // pointer to off-heap memory
private final Cleaner.Cleanable cleanable;
public NativeBuffer(int size) {
this.nativePtr = allocateNative(size); // hypothetical native call
// Register cleanup action — must NOT reference 'this' to avoid memory leak
long ptr = this.nativePtr;
this.cleanable = CLEANER.register(this, () -> freeNative(ptr));
}
@Override
public void close() {
cleanable.clean(); // explicit close — runs cleanup immediately
}
private static native long allocateNative(int size);
private static native void freeNative(long ptr);
}
// Use with try-with-resources — close() called automatically
try (NativeBuffer buf = new NativeBuffer(1024)) {
// use buf
} // close() called here; Cleaner acts as a safety net if close() was missed
Note: Prefer explicit close()