Introduction
Every class in Java ultimately inherits from the java.lang.Object class.
For example:
1class Student { 2}
is effectively part of the following inheritance hierarchy:
1Object 2 ↑ 3Student
You do not normally need to explicitly write extends Object because Java provides this inheritance automatically for classes that do not explicitly extend another class.
The Object class provides fundamental methods that are inherited by Java objects, including:
1equals() 2hashCode() 3toString() 4getClass() 5clone() 6wait() 7notify() 8notifyAll()
Understanding these methods is important because they are frequently used by the Java Collections Framework, HashMap, HashSet, testing libraries, logging systems, frameworks, and enterprise applications.
For example, when you write:
1System.out.println(student);
Java automatically calls:
1student.toString();
Similarly, when an object is used as a key in a HashMap or stored in a HashSet, the equals() and hashCode() contract becomes important.
In this tutorial, you will learn:
- What the
Objectclass is - How
equals()works - How
hashCode()works - The relationship between
equals()andhashCode() - How to override
toString() - How
clone()works - Why
clone()should be used carefully - How
getClass()works - How
instanceofworks - Nested classes
- Static nested classes
- Inner classes
- Anonymous classes
- Modern Java alternatives
- Best practices
- Practical projects and exercises
What Is the Object Class?
Object is the root class of the Java class hierarchy.
It belongs to:
1java.lang.Object
The java.lang package is automatically imported by Java, so you do not normally need:
1import java.lang.Object;
Consider:
1class Student { 2 private String name; 3}
The class can be viewed conceptually as:
1class Student extends Object { 2 private String name; 3}
If a class already extends another class:
1class Person { 2} 3 4class Student extends Person { 5}
the inheritance chain becomes:
1Object 2 ↑ 3Person 4 ↑ 5Student
Therefore, every ordinary Java class ultimately inherits from Object.
Important Object Methods
Some of the most important methods inherited from Object are:
| Method | Purpose |
|---|---|
equals() | Determines logical equality |
hashCode() | Produces a hash value used by hash-based collections |
toString() | Provides a string representation of an object |
getClass() | Returns the runtime class of an object |
clone() | Performs a field-level copy when cloning is supported |
wait() | Causes the current thread to wait |
notify() | Wakes one waiting thread |
notifyAll() | Wakes all waiting threads |
The methods wait(), notify(), and notifyAll() are related to Java's intrinsic monitor-based synchronization and are generally encountered in multithreading rather than ordinary object-oriented programming.
equals() Method
The equals() method is used to determine whether two objects should be considered logically equal.
Its declaration in Object is:
1public boolean equals(Object obj)
The default implementation in Object effectively compares object identity.
For example:
1class Student { 2}
Now create two objects:
1Student first = new Student(); 2Student second = new Student(); 3 4System.out.println(first.equals(second));
Output:
1false
The objects contain no fields, but they are still two different object instances.
Object Identity vs Logical Equality
This distinction is extremely important.
Object Identity
Identity asks:
Are these the exact same object?
This can be checked with:
1first == second
For example:
1Student first = new Student(); 2Student second = first; 3 4System.out.println(first == second);
Output:
1true
Both variables refer to the same object.
Logical Equality
Logical equality asks:
Do these two different objects represent the same value?
For example, two Student objects might be considered equal when they have the same student ID.
This is where overriding equals() becomes useful.
Overriding equals()
Suppose a student is identified by a unique ID.
1class Student { 2 3 private final int id; 4 private final String name; 5 6 public Student(int id, String name) { 7 this.id = id; 8 this.name = name; 9 } 10 11 public int getId() { 12 return id; 13 } 14 15 public String getName() { 16 return name; 17 } 18 19 @Override 20 public boolean equals(Object obj) { 21 22 if (this == obj) { 23 return true; 24 } 25 26 if (!(obj instanceof Student other)) { 27 return false; 28 } 29 30 return id == other.id; 31 } 32}
Now:
1Student first = new Student(101, "Ankit"); 2Student second = new Student(101, "Rahul"); 3 4System.out.println(first.equals(second));
Output:
1true
The names are different, but according to our application's definition, the student ID determines equality.
Understanding the equals() Contract
A correctly implemented equals() method should follow important rules.
Reflexive
An object must equal itself:
1x.equals(x)
should be:
1true
Symmetric
If:
1x.equals(y)
is true, then:
1y.equals(x)
should also be true.
Transitive
If:
1x equals y 2y equals z
then:
1x equals z
should also be true.
Consistent
Repeated calls should produce the same result as long as the relevant object state has not changed.
Null
For a non-null object:
1x.equals(null)
should return:
1false
These rules are important when implementing equality for domain objects.
hashCode() Method
The hashCode() method returns an integer hash value for an object.
Its declaration is:
1public int hashCode()
Hash codes are particularly important for collections such as:
1HashMap 2HashSet 3Hashtable
For example:
1Student student = new Student(101, "Ankit"); 2 3System.out.println(student.hashCode());
The exact integer value should not be treated as a permanent identifier. Hash values can vary between implementations or program executions depending on how a class implements the method.
equals() and hashCode() Contract
The most important rule is:
If two objects are equal according to
equals(), they must return the samehashCode().
However:
Two objects having the same hash code do not necessarily mean they are equal.
This is because different objects can have the same hash value. Such a situation is called a hash collision.
Why hashCode() Matters
Consider a HashSet:
1Set<Student> students = new HashSet<>(); 2 3students.add(new Student(101, "Ankit")); 4students.add(new Student(101, "Ankit")); 5 6System.out.println(students.size());
If equals() and hashCode() are implemented consistently, the set can recognize the two objects as equal.
Therefore, when overriding equals(), you should normally override hashCode() using the same fields that participate in equality.
Using Objects.hash()
Java provides the Objects utility class for creating hash codes conveniently.
1import java.util.Objects; 2 3@Override 4public int hashCode() { 5 return Objects.hash(id); 6}
For multiple fields:
1@Override 2public int hashCode() { 3 return Objects.hash(id, name); 4}
The fields used here should match the fields used by equals().
Complete equals() and hashCode() Example
A modern implementation can look like this:
1import java.util.Objects; 2 3public final class Student { 4 5 private final int id; 6 private final String name; 7 8 public Student(int id, String name) { 9 10 this.id = id; 11 this.name = Objects.requireNonNull( 12 name, 13 "name must not be null" 14 ); 15 } 16 17 public int getId() { 18 return id; 19 } 20 21 public String getName() { 22 return name; 23 } 24 25 @Override 26 public boolean equals(Object obj) { 27 28 if (this == obj) { 29 return true; 30 } 31 32 if (!(obj instanceof Student other)) { 33 return false; 34 } 35 36 return id == other.id; 37 } 38 39 @Override 40 public int hashCode() { 41 return Integer.hashCode(id); 42 } 43 44 @Override 45 public String toString() { 46 return "Student{" + 47 "id=" + id + 48 ", name='" + name + '\'' + 49 '}'; 50 } 51}
This implementation uses the student ID consistently for:
1equals() 2hashCode()
That consistency is important when Student objects are used in hash-based collections.
toString() Method
The toString() method returns a string representation of an object.
Consider:
1class Student { 2 3 private final int id; 4 5 Student(int id) { 6 this.id = id; 7 } 8}
If you print:
1Student student = new Student(101); 2 3System.out.println(student);
you may see output similar to:
1Student@5acf9800
The exact value is not important. The default representation usually does not provide useful business information.
Overriding toString()
You can provide a meaningful representation:
1@Override 2public String toString() { 3 4 return "Student{id=" + id + "}"; 5}
Now:
1System.out.println(student);
might produce:
1Student{id=101}
This is especially useful during:
- Debugging
- Logging
- Testing
- Development
- Inspecting collection contents
Modern toString() Example
For a class with several fields:
1@Override 2public String toString() { 3 4 return "Student{" + 5 "id=" + id + 6 ", name='" + name + '\'' + 7 '}'; 8}
Output:
1Student{id=101, name='Ankit'}
Avoid putting passwords, access tokens, authentication credentials, or other sensitive information into toString() because objects are frequently logged during debugging.
getClass() Method
The getClass() method returns the runtime class of an object.
Example:
1Student student = new Student(101, "Ankit"); 2 3System.out.println(student.getClass());
Output:
1class Student
You can also retrieve the class name:
1System.out.println( 2 student.getClass().getSimpleName() 3);
Output:
1Student
This is useful for:
- Reflection
- Debugging
- Framework internals
- Runtime type inspection
instanceof Operator
instanceof checks whether an object is an instance of a particular class or interface.
Consider:
1class Animal { 2} 3 4class Dog extends Animal { 5}
Now:
1Animal animal = new Dog(); 2 3System.out.println(animal instanceof Dog); 4System.out.println(animal instanceof Animal);
Output:
1true 2true
The actual object is a Dog, even though the variable type is Animal.
instanceof with Pattern Matching
Modern Java provides pattern matching for instanceof.
Instead of:
1if (animal instanceof Dog) { 2 3 Dog dog = (Dog) animal; 4 5 dog.bark(); 6}
you can write:
1if (animal instanceof Dog dog) { 2 dog.bark(); 3}
This is shorter and avoids an unnecessary explicit cast.
instanceof and null
An important detail is that:
1null instanceof Student
returns:
1false
For example:
1Student student = null; 2 3if (student instanceof Student) { 4 5 System.out.println("Student object"); 6 7} else { 8 9 System.out.println("Not a Student object"); 10}
Output:
1Not a Student object
clone() Method
The clone() method comes from Object.
Its purpose is to create a copy of an object.
However, Java's cloning mechanism has several design limitations, so it should be used carefully.
A class must implement Cloneable to indicate that cloning is supported through Object.clone().
Example:
1class Student implements Cloneable { 2 3 private final int id; 4 private String name; 5 6 public Student(int id, String name) { 7 8 this.id = id; 9 this.name = name; 10 } 11 12 @Override 13 public Student clone() { 14 15 try { 16 17 return (Student) super.clone(); 18 19 } catch (CloneNotSupportedException e) { 20 21 throw new AssertionError(e); 22 } 23 } 24}
Usage:
1Student original = 2 new Student(101, "Ankit"); 3 4Student copy = original.clone(); 5 6System.out.println(copy);
Shallow Copy
Object.clone() performs a field-level copy.
For primitive fields, the values are copied.
For reference fields, the references are copied rather than recursively cloning the referenced objects.
Consider:
1class Address { 2 3 String city; 4 5 Address(String city) { 6 this.city = city; 7 } 8} 9 10class Person implements Cloneable { 11 12 String name; 13 Address address; 14 15 Person(String name, Address address) { 16 this.name = name; 17 this.address = address; 18 } 19 20 @Override 21 protected Person clone() 22 throws CloneNotSupportedException { 23 24 return (Person) super.clone(); 25 } 26}
If you clone Person, both objects can reference the same Address.
That is a shallow copy.
Conceptually:
1Original Person 2 │ 3 └── Address ──► Delhi 4 5Cloned Person 6 │ 7 └── Address ──► same Address
Changing the shared Address can therefore affect both objects.
Deep Copy
A deep copy creates independent copies of mutable nested objects.
Conceptually:
1Original Person 2 │ 3 └── Address ──► Delhi 4 5Cloned Person 6 │ 7 └── Address ──► Delhi
The two Address objects are different instances.
Prefer Copy Constructors
For new Java code, a copy constructor is often clearer than clone().
Example:
1class Student { 2 3 private final int id; 4 private String name; 5 6 public Student(int id, String name) { 7 this.id = id; 8 this.name = name; 9 } 10 11 public Student(Student other) { 12 13 this.id = other.id; 14 this.name = other.name; 15 } 16}
Usage:
1Student original = 2 new Student(101, "Ankit"); 3 4Student copy = 5 new Student(original);
A copy constructor makes the copying behavior explicit and can be customized easily.
Other alternatives include:
- Static factory methods
- Builder-based copying
- Explicit copy methods
- Serialization-based approaches in specific scenarios
Nested Classes
A nested class is a class declared inside another class.
Java supports several forms:
1Nested Classes 2├── Static Nested Class 3├── Inner Class 4├── Local Class 5└── Anonymous Class
Nested classes are useful when a helper class is strongly related to its enclosing class.
Inner Class
A non-static nested class is called an inner class.
Example:
1class University { 2 3 private String name = "Tech University"; 4 5 class Department { 6 7 void display() { 8 9 System.out.println( 10 "University: " + name 11 ); 12 } 13 } 14}
Creating the inner class:
1University university = 2 new University(); 3 4University.Department department = 5 university.new Department(); 6 7department.display();
Output:
1University: Tech University
An inner class instance is associated with an instance of the outer class.
Static Nested Class
A static nested class does not require an instance of the outer class.
Example:
1class University { 2 3 static class Department { 4 5 void display() { 6 7 System.out.println( 8 "Computer Science Department" 9 ); 10 } 11 } 12}
Usage:
1University.Department department = 2 new University.Department(); 3 4department.display();
Output:
1Computer Science Department
A static nested class is often useful for helper types that logically belong to the enclosing class but do not need access to a particular outer-class instance.
Inner Class vs Static Nested Class
| Feature | Inner Class | Static Nested Class |
|---|---|---|
static keyword | No | Yes |
| Requires outer instance | Yes | No |
| Can access outer instance fields | Yes | Not directly |
| Useful for | Instance-specific helpers | Closely related independent helpers |
Local Classes
A local class is declared inside a method, constructor, or block.
Example:
1public class NotificationService { 2 3 public void send() { 4 5 class Formatter { 6 7 String format(String message) { 8 9 return "[Notification] " + message; 10 } 11 } 12 13 Formatter formatter = new Formatter(); 14 15 System.out.println( 16 formatter.format("Hello Java") 17 ); 18 } 19}
Output:
1[Notification] Hello Java
Local classes are useful when a helper type is needed only within a small scope.
Anonymous Classes
An anonymous class is a class without an explicit name.
It can be useful for creating a one-time implementation.
Example:
1interface Greeting { 2 3 void sayHello(); 4}
Anonymous implementation:
1Greeting greeting = new Greeting() { 2 3 @Override 4 public void sayHello() { 5 6 System.out.println( 7 "Hello from Java" 8 ); 9 } 10}; 11 12greeting.sayHello();
Output:
1Hello from Java
Anonymous Classes vs Lambda Expressions
If an interface is a functional interface with exactly one abstract method, a lambda is usually simpler.
Instead of:
1Greeting greeting = new Greeting() { 2 3 @Override 4 public void sayHello() { 5 6 System.out.println("Hello Java"); 7 } 8};
you can use:
1Greeting greeting = 2 () -> System.out.println("Hello Java");
Anonymous classes are still useful when you need:
- Multiple overridden methods
- Additional instance fields
- More complex one-time behavior
- A specific anonymous subclass implementation
Object Methods and Collections
Understanding Object becomes especially important when working with collections.
Consider:
1Set<Student> students = new HashSet<>(); 2 3students.add( 4 new Student(101, "Ankit") 5); 6 7students.add( 8 new Student(101, "Ankit") 9);
For the set to correctly identify logically equal students, the Student class needs a consistent implementation of:
1equals() 2hashCode()
Similarly, toString() helps when inspecting the collection:
1System.out.println(students);
This is one reason understanding the Object class is essential before moving deeper into Java Collections Framework topics.
Complete Practical Example
The following example combines equals(), hashCode(), and toString().
1import java.util.HashSet; 2import java.util.Objects; 3import java.util.Set; 4 5final class Book { 6 7 private final int id; 8 private final String title; 9 10 public Book(int id, String title) { 11 12 if (id <= 0) { 13 throw new IllegalArgumentException( 14 "Book ID must be positive." 15 ); 16 } 17 18 this.id = id; 19 this.title = Objects.requireNonNull( 20 title, 21 "Title must not be null." 22 ); 23 } 24 25 public int getId() { 26 return id; 27 } 28 29 public String getTitle() { 30 return title; 31 } 32 33 @Override 34 public boolean equals(Object obj) { 35 36 if (this == obj) { 37 return true; 38 } 39 40 if (!(obj instanceof Book other)) { 41 return false; 42 } 43 44 return id == other.id; 45 } 46 47 @Override 48 public int hashCode() { 49 50 return Integer.hashCode(id); 51 } 52 53 @Override 54 public String toString() { 55 56 return "Book{" + 57 "id=" + id + 58 ", title='" + title + '\'' + 59 '}'; 60 } 61} 62 63public class Main { 64 65 public static void main(String[] args) { 66 67 Book first = 68 new Book(101, "Java Programming"); 69 70 Book second = 71 new Book(101, "Java Programming"); 72 73 System.out.println( 74 "Equal: " + first.equals(second) 75 ); 76 77 System.out.println( 78 "Hash codes: " 79 + first.hashCode() 80 + " / " 81 + second.hashCode() 82 ); 83 84 System.out.println(first); 85 86 Set<Book> books = new HashSet<>(); 87 88 books.add(first); 89 books.add(second); 90 91 System.out.println( 92 "Number of books: " + books.size() 93 ); 94 } 95}
Expected output structure:
1Equal: true 2Hash codes: <same-value> / <same-value> 3Book{id=101, title='Java Programming'} 4Number of books: 1
The exact numeric hash-code value is not guaranteed and should not be hard-coded into tests or documentation.
Common Mistakes
Overriding equals() Without hashCode()
This is one of the most common mistakes.
Incorrect:
1@Override 2public boolean equals(Object obj) { 3 // ... 4}
without implementing:
1@Override 2public int hashCode() { 3 // ... 4}
This can cause unexpected behavior in HashSet and HashMap.
Comparing Strings with ==
Do not use:
1if (name1 == name2)
when you want to compare string contents.
Use:
1if (name1.equals(name2))
or, when null safety is needed:
1if (Objects.equals(name1, name2))
Using Mutable Fields in hashCode()
Be careful when fields used by equals() and hashCode() can change while an object is stored in a hash-based collection.
For example:
1Set<Student> students = new HashSet<>();
If a student's equality-defining field changes after insertion, the collection may no longer be able to locate the object correctly.
Immutable equality-defining fields are often a safer design.
Using clone() Without Understanding Shallow Copy
clone() does not automatically create a deep copy of an object's entire object graph.
Always understand whether your nested objects are mutable and shared.
Catching CloneNotSupportedException Incorrectly
If a class implements Cloneable and directly invokes super.clone(), the checked exception can be handled or exposed according to the design.
Do not add cloning merely because Object provides the method. Prefer simpler copying mechanisms when appropriate.
Best Practices
- Override
equals()andhashCode()together. - Use the same equality-defining fields in both methods.
- Keep equality stable while objects are stored in hash-based collections.
- Override
toString()for useful debugging output. - Never expose sensitive information through
toString(). - Use
instanceofcarefully before downcasting. - Prefer pattern matching for
instanceofin modern Java where appropriate. - Prefer copy constructors or explicit copy methods over
clone()for new designs. - Use static nested classes when the nested type does not need an outer instance.
- Use inner classes when the nested object genuinely depends on an outer instance.
- Prefer lambdas for simple functional-interface implementations.
- Do not use exceptions as a replacement for ordinary control flow.
- Use immutable objects where practical, especially when they participate in equality and hashing.
Object Class Quick Reference
| Method | Purpose | Common Use |
|---|---|---|
equals() | Logical equality | Domain objects |
hashCode() | Hash value | HashMap, HashSet |
toString() | String representation | Debugging and logging |
getClass() | Runtime class | Reflection and debugging |
clone() | Field-level object copy | Legacy/specific cloning designs |
wait() | Wait for notification | Synchronization |
notify() | Wake one waiting thread | Synchronization |
notifyAll() | Wake waiting threads | Synchronization |
Practice Project: Library Management System
Build a small library application using the concepts covered in this tutorial.
Requirements
Create a Book class containing:
1id 2title 3author
Implement:
1equals() 2hashCode() 3toString()
Use the book ID as the equality identifier.
Then create a:
1Set<Book>
and add several books, including duplicate IDs.
Example:
1Set<Book> books = new HashSet<>(); 2 3books.add( 4 new Book( 5 101, 6 "Java Programming", 7 "James" 8 ) 9); 10 11books.add( 12 new Book( 13 102, 14 "Clean Code", 15 "Robert" 16 ) 17); 18 19books.add( 20 new Book( 21 101, 22 "Java Programming", 23 "James" 24 ) 25);
Your program should demonstrate that the logically duplicated book is treated according to the equals() and hashCode() implementation.
Practice Exercises
Exercise 1: Product Class
Create a Product class containing:
1id 2name 3price
Override:
1equals() 2hashCode() 3toString()
Use the product ID as the equality identifier.
Exercise 2: Employee Equality
Create an Employee class where two employees are considered equal when their employee IDs are the same.
Store employees in:
1HashSet<Employee>
Test the behavior with duplicate employee IDs.
Exercise 3: Student Type Checking
Create:
1Person 2Student 3Teacher
Use instanceof and pattern matching to identify the runtime type of different objects.
Exercise 4: Copy Constructor
Create a Student class with a copy constructor.
Create an original student and a copied student, then modify the copy and verify that the objects are independent where mutable state is involved.
Exercise 5: Nested Class
Create a:
1University
class with:
1Department
as an inner class.
Display university and department information.
Exercise 6: Anonymous Class
Create a:
1PaymentProcessor
interface containing one method:
1processPayment()
Implement it using an anonymous class.
Then create a second implementation using a lambda and compare the two approaches.
Summary
The Object class is the foundation of Java's object model. Every ordinary Java class ultimately inherits from it.
In this tutorial, you learned:
- Why every Java class ultimately extends
Object - How object identity differs from logical equality
- How
equals()works - The contract that
equals()must follow - Why
equals()andhashCode()must be implemented consistently - How hash-based collections depend on equality and hashing
- How
toString()improves debugging and logging - How
getClass()identifies an object's runtime class - How
instanceofchecks runtime types - How pattern matching simplifies type checks
- How
clone()works - The difference between shallow and deep copying
- Why copy constructors are often preferable to
clone() - Inner classes
- Static nested classes
- Local classes
- Anonymous classes
- Lambda expressions as an alternative to simple anonymous implementations
- Common mistakes and best practices
Understanding these concepts gives you a strong foundation for Java Collections, Generics, Streams, Spring Boot, Hibernate, multithreading, and enterprise Java development.