Skip to main content

Java Object Class: Methods and Contracts

In Java, the java.lang.Object class is the ultimate root of the class hierarchy. Every class in Java, whether user-defined or built-in, implicitly or explicitly inherits from Object. If a class does not specify a superclass using the extends keyword, the compiler automatically adds extends Object.

This design ensures that all Java objects share a common set of foundational behaviors.


1. Summary of Object Methodsโ€‹

The Object class defines 12 methods (including registerNatives). Below is a summary of these methods:

Method SignatureAccess / ModifiersDescription
private static native void registerNatives()private static nativeRegisters native C/C++ method mappings for the JVM.
public final native Class<?> getClass()public final nativeReturns the runtime class of the object.
public native int hashCode()public nativeReturns a hash code value for the object (used in hash tables).
public boolean equals(Object obj)publicCompares two objects for reference equality by default.
protected native Object clone() throws CloneNotSupportedExceptionprotected nativeCreates and returns a shallow copy of the object.
public String toString()publicReturns a string representation of the object.
public final native void notify()public final nativeWakes up a single thread waiting on the object's monitor.
public final native void notifyAll()public final nativeWakes up all threads waiting on the object's monitor.
public final void wait() throws InterruptedExceptionpublic finalCauses the current thread to wait until notified or interrupted.
public final native void wait(long timeoutMillis) throws InterruptedExceptionpublic final nativeCauses the current thread to wait up to a specified time.
public final void wait(long timeout, int nanos) throws InterruptedExceptionpublic finalCauses the current thread to wait with nanosecond precision.
protected void finalize() throws Throwableprotected (Deprecated)Called by GC before reclaiming the object's memory.

2. Deep Dive & Contractsโ€‹

2.1 getClass()โ€‹

Returns the java.lang.Class object that represents the runtime class of this object.

Object obj = "Hello Antigravity";
Class<?> clazz = obj.getClass();
System.out.println(clazz.getName()); // Outputs: java.lang.String

Key Considerationsโ€‹

  • Reflection: It is the primary entry point for Java Reflection, allowing dynamic inspection of fields, methods, constructors, and annotations.
  • Type Checking: Useful to determine exact runtime types.
  • getClass() vs instanceof:
    • getClass() == Other.class checks for exact type match (subclasses will return false).
    • instanceof checks if the object is of the specified type or any of its subclasses (respects the Liskov Substitution Principle).

2.2 equals(Object obj)โ€‹

Determines if another object is "equal to" this one. By default, the implementation compares memory addresses (reference equality):

public boolean equals(Object obj) {
return (this == obj);
}

When overriding equals(), you must satisfy the equivalence relation contract:

  • Reflexive: x.equals(x) must return true.
  • Symmetric: x.equals(y) must return true if and only if y.equals(x) returns true.
  • Transitive: If x.equals(y) is true and y.equals(z) is true, then x.equals(z) must be true.
  • Consistent: Multiple invocations of x.equals(y) must consistently return true or false, provided no information used in the comparison is modified.
  • Null-safety: x.equals(null) must return false.

2.3 hashCode()โ€‹

Returns a hash code value (an integer) for the object. This integer is used by hash-based data structures such as HashMap, HashSet, and Hashtable.

The equals and hashCode Contractโ€‹

If you override equals(), you must override hashCode(). The contract states:

  1. If two objects are equal according to the equals(Object) method, their hashCode() values must be equal.
  2. If two objects are unequal according to equals(Object), their hashCode() values do not have to be distinct. However, generating distinct hash codes for distinct objects improves search performance in hash-based collections by reducing collisions.

[!WARNING] Common Bug: If you override equals without hashCode, equal instances will have different hash codes. When stored in a HashMap, they will end up in different buckets, preventing retrieval and causing duplicate keys or memory leaks.

Standard Implementation Patternโ€‹

import java.util.Objects;

public class Employee {
private final int id;
private final String name;

public Employee(int id, String name) {
this.id = id;
this.name = name;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Employee employee = (Employee) o;
return id == employee.id && Objects.equals(name, employee.name);
}

@Override
public int hashCode() {
return Objects.hash(id, name);
}
}

2.4 toString()โ€‹

Returns a string representation of the object. The default implementation is:

public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Best Practicesโ€‹

  • Readability: Always override toString() in domain objects and DTOs to return a concise, informative representation of the object's state.
  • Security: Do not include sensitive information (e.g., passwords, SSNs, personal credentials) in toString() to avoid accidental logging or exposure.
  • Modern Java: Records automatically generate a readable toString() implementation.

2.5 clone()โ€‹

Creates and returns a copy of this object. By default, Object.clone() is a native method that performs a shallow copy (copies primitive fields and reference memory addresses, but does not duplicate the referenced objects themselves).

protected native Object clone() throws CloneNotSupportedException;

The Cloneable Marker Interfaceโ€‹

  • A class must implement the java.lang.Cloneable interface to signal to Object.clone() that it is legal to make a copy of its instances.
  • If a class calls clone() without implementing Cloneable, it throws a CloneNotSupportedException.

Pitfalls of clone()โ€‹

  • No Constructor Call: It copies memory directly without invoking constructors, bypassing initialization logic.
  • Final Fields: It does not work well with final fields pointing to mutable objects, because the final reference cannot be reassigned during deep copying inside clone().
  • Checked Exception: Throws CloneNotSupportedException, forcing boilerplate try-catch blocks.

Modern Alternativesโ€‹

Prefer Copy Constructors or Static Factory Methods for object copying:

// Copy Constructor
public class User {
private String username;
private Profile profile;

public User(User other) {
this.username = other.username;
this.profile = new Profile(other.profile); // Deep copy manually
}
}

2.6 Concurrency Methods (wait, notify, notifyAll)โ€‹

These methods are used for inter-thread communication in Java's built-in monitor lock synchronization model.

  • wait(): Suspends the current thread, releasing the lock on the object's monitor, until another thread invokes notify() or notifyAll() on this object.
  • notify(): Wakes up a single thread that is waiting on this object's monitor.
  • notifyAll(): Wakes up all threads waiting on this object's monitor.
synchronized (lockObject) {
while (conditionNotMet) {
lockObject.wait(); // Releases lockObject and enters WAITING state
}
// Perform action
}

Crucial Rulesโ€‹

  1. Must Hold Monitor: You can only call these methods from a synchronized block or synchronized method of the target object. Otherwise, the JVM throws an IllegalMonitorStateException at runtime.
  2. Loop Condition: Always call wait() inside a while loop, never an if statement. This protects against spurious wakeups (where a thread wakes up without being explicitly notified).
  3. Prefer notifyAll(): Calling notify() wakes up only one thread, which might not be the thread capable of making progress, leading to deadlocks. notifyAll() is safer as it wakes all waiting threads to re-evaluate conditions.

Modern Alternativesโ€‹

Use java.util.concurrent utilities:

  • Lock and Condition: ReentrantLock with Condition.await() and Condition.signalAll() provides granular control.
  • High-Level Utilities: Use BlockingQueue, CountDownLatch, CyclicBarrier, or Semaphore to avoid low-level thread wait/notify mechanics altogether.

2.7 finalize()โ€‹

Invoked by the Garbage Collector (GC) on an object when GC determines that there are no more active references to it.

protected void finalize() throws Throwable { }

[!CAUTION] Do Not Use finalize(): finalize() was deprecated in Java 9 and is marked for removal in future Java versions. It has major design problems:

  • Unpredictable Execution: The JVM does not guarantee when or even if finalize() will run.
  • Performance Overhead: Classes overriding finalize() slow down garbage collection allocation and sweeping.
  • Security Risks: Enables "Finalizer Attacks" where a partially constructed object can be resurrected.
  • Resource Leaks: If finalize() throws an exception, the exception is ignored, leaving resources unclosed.

Modern Alternativesโ€‹

  • AutoCloseable with Try-With-Resources: Implement AutoCloseable and release resources in the close() method:
    try (MyResource resource = new MyResource()) {
    resource.execute();
    } // Automatically calls resource.close() here
  • java.lang.ref.Cleaner or PhantomReference: For advanced non-heap resource management (e.g., direct buffers, native handles), use Cleaner (introduced in Java 9).

2.8 registerNatives()โ€‹

A private static native method called during the static initialization of the Object class.

private static native void registerNatives();
static {
registerNatives();
}

It registers JVM native functions (written in C/C++) that implement standard methods like hashCode(), clone(), getClass(), and wait/notify routines, linking Java method signatures directly to C/C++ implementations in the virtual machine.


3. Compare Nextโ€‹