Back to Blog
Java

Understanding Java Object Class Methods

java object class methods: Learn the essential methods of Java's Object class, including equals, hashCode, toString, clone, and thread coordination, with practical ove...

Object Classequals and hashCodetoString OverrideJava ConcurrencyClone Method
Diagram showing Java Object class methods like equals, hashCode, toString, and clone connected to a central Object node.

Every Java class directly or indirectly inherits from Object, the root of the class hierarchy. The Object class provides a set of methods that define fundamental behavior for all objects. Understanding these java object class methods is critical for writing correct equals and hashCode implementations, producing readable toString output, and managing object lifecycle and thread coordination. This article explains the purpose of each method, the contracts they impose, and how to override them safely in real-world code.

The Object Class as the Root of the Class Hierarchy

When you define a class in Java without an explicit extends clause, the compiler automatically makes it extend java.lang.Object. This means every object inherts the following methods:

  • toString()
  • equals(Object)
  • hashCode()
  • getClass()
  • clone()
  • finalize() (deprecated)
  • wait(), notify(), notifyAll()
  • getClass() and several others

These methods serve as the baseline for object behavior. Some are meant to be overridden, while others are final or designed for internal JVM use. For example, getClass() is final and returns the runtime class of the object, which is often used in reflection and type checks.

The equals Method and Its Contract

The default implementation of equals(Object) in Object uses reference equality: it returns true only if this and the argument refer to the same object. For many value-based classes, such as String, Integer, or custom domain objects, you need logical equality. Overriding equals is the way to define what it means for two objects to be equal.

The equals method must follow a strict 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) and y.equals(z), then x.equals(z).
  • Consistent: repeated calls must return the same result, assuming no fields used in the comparison have changed.
  • Non-null: x.equals(null) must return false.

Here is a typical override for a Person class with name and age fields:

public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && name.equals(person.name); } }

Notice the use of getClass() to ensure the objects are of the same class. This avoids ClassCastException and maintains symmetry when subclasses are involved. An alternative is to use instanceof and allow subclass equality, but that can break symmetry if the subclass overrides equals. The getClass() approach is stricter and often safer for immutable value objects.

The hashCode Method and Its Relationship with equals

The hashCode method returns an integer hash code that is used in hash-based collections like HashMap, HashSet, and Hashtable. The contract between equals and hashCode is critical:

  • If two objects are equal according to equals, they must have the same hashCode.
  • If two objects are unequal, they may have the same hashCode (collision), but the hash function should minimize collisions for performance.

Failing to override hashCode when you override equals will break hash-based collections. For example, if you put a Person object into a HashSet and later look it up with an equal but not identical object, the set will not find it because the default hashCode is based on memory address.

A standard implementation uses a prime number and combines field hashes:

@Override public int hashCode() { int result = name.hashCode(); result = 31 * result + age; return result; }

The multiplier 31 is a common choice because it is an odd prime and the multiplication can be optimized by the JVM. The exact value is not mandated by the contract, but consistency is essential.

When fields are nullable, you should use Objects.hashCode(field) to avoid null pointer exceptions. The Objects.hash method provides a convenient way to compute a combined hash:

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

This is concise and handles null fields safely. However, it creates an array internally, so for performance-critical code you may prefer a manual computation.

Overriding toString for Readable Output

The default toString implementation returns a string in the form ClassName@hashcode, which is rarely useful for debugging or logging. Overriding toString gives you a readable representation of an object's state. This is especially valuable in logging frameworks and error messages.

A good toString should include the class name and the most relevant fields. For Person:

@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; }

If you use a modern IDE, you can generate toString, equals, and hashCode automatically. For a large object graph, consider using a builder or a library like ToStringBuilder from Apache Commons Lang, which handles nulls and formatting consistently.

The clone Method and Its Limitations

The clone method creates a shallow copy of an object. It is declared protected in Object, so to use it you must override it and implement the Cloneable marker interface. Without Cloneable, calling clone throws CloneNotSupportedException.

Shallow copy means that the new object's fields are copied, but if a field is a reference, both the original and the copy point to the same object. For immutable fields this is fine, but for mutable objects you often need a deep copy.

Here is a minimal clone override:

@Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }

This only works if the class implements Cloneable. A more practical approach is to use a copy constructor or a static factory method, which gives you control over the copy semantics and avoids the pitfalls of clone. Many developers consider clone broken by design because it is difficult to implement correctly for deep copies and it bypasses constructors. In modern Java, you can use Objects.copyOf for collections, but for custom objects, prefer copy constructors or serialization-based deep copy if needed.

wait, notify, and notifyAll for Thread Coordination

The wait, notify, and notifyAll methods are used for inter-thread communication when using intrinsic locks (synchronized blocks). These methods must be called from within a synchronized context; otherwise, they throw IllegalMonitorStateException.

  • wait() causes the current thread to release the monitor and wait until another thread calls notify() or notifyAll() on the same object.
  • notify() wakes up one waiting thread (chosen arbitrarily).
  • notifyAll() wakes up all waiting threads.

A typical producer-consumer pattern uses these methods. However, they are low-level and error-prone. The java.util.concurrent package provides higher-level abstractions like BlockingQueue, CountDownLatch, and Condition that are safer and more expressive. In most new code, you should avoid direct wait/notify usage unless you are implementing a custom synchronization primitive.

finalize and Its Deprecation

The finalize method was intended to perform cleanup before an object is garbage collected. It has been deprecated since Java 9 and is slated for removal. The reasons are:

  • Finalizers can cause performance issues because they delay garbage collection.
  • They run in an unpredictable order and at an unpredictable time.
  • They can resurrect objects, leading to memory leaks.

If you need to release resources like file handles or network connections, use try-with-resources or explicitly call close() in a finally block. The Cleaner and PhantomReference classes offer a safer alternative, but they are still advanced and rarely needed. For most applications, avoid overriding finalize entirely.

Using the Objects Utility Class to Simplify Overrides

The java.util.Objects class provides static helper methods that reduce boilerplate and null-safety issues when overriding equals, hashCode, and toString. For example:

  • Objects.equals(a, b) returns true if both are equal or both are null.
  • Objects.hashCode(obj) returns 0 for null.
  • Objects.hash(obj...) combines multiple values into a hash.
  • Objects.toString(obj, nullDefault) returns a default string if the object is null.

Here is a complete Person class using these utilities:

import java.util.Objects; public class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); } @Override public int hashCode() { return Objects.hash(name, age); } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }

Using Objects.equals for nullable fields is a best practice because it avoids a manual null check and keeps the code concise. This approach also makes the equals method symmetric and transitive when the class hierarchy is properly managed.

When you design a class that will be used as a key in a hash-based collection, the equals and hashCode contract is not just a suggestion—it is a requirement. Violating it leads to subtle bugs that are hard to diagnose. Always test your overrides with both equal and unequal objects, and consider using a unit testing framework to verify the contract. For performance-sensitive code, remember that Objects.hash allocates an array, so a manual hash computation may be preferable if the method is called frequently in a tight loop. The tradeoff between readability and performance should guide your choice, but correctness always comes first.

java object class methods: Practical Usage and Code Examples | RYUSLOG DEV