Java Object Class

In the last lesson you learned about Java abstract classes. Now here is a surprising fact: every class you write already has methods you never wrote, like toString and equals. They come from a hidden parent called Object. Let’s learn it.

🤔 The problem: where do these methods come from?

Write a tiny class with nothing in it, and it still has methods you can call.

class Student {
}
public class Main {
public static void main(String[] args) {
Student s = new Student();
System.out.println(s.toString()); // works! but where is toString defined?
}
}
  • No fields, no methods. It looks completely empty.
  • Yet toString runs. So do equals and hashCode.
  • You never wrote them. So who did?

The class is not really empty. Java quietly gave it a parent, and that parent already had these methods.

🌳 Object is the root of every class

Object is a built-in class that sits at the very top of the class family tree.

  • Its full name is java.lang.Object. Every other class sits below it.
  • If a class extends nothing, Java makes it extend Object for you.
  • You never type extends Object. Java adds it.
  • Even Dog extends Animal reaches Object, because Animal extends Object.

So the two lines below mean the same thing.

class Student { // you write this
}
class Student extends Object { // Java treats it as this
}

One root for everything

Every class you ever use or write, including String, ArrayList, and your own classes, traces back to java.lang.Object. There is exactly one root, and this is it.

🧰 The methods every object inherits

Every object comes with a small set of ready-made methods.

  • toString() gives a text version of the object.
  • equals(Object other) checks if this object is “equal” to another one.
  • hashCode() gives an int used by hash-based collections like HashMap.
  • getClass() tells you the real class of the object at runtime.
  • clone() makes a copy of the object.
  • finalize() is deprecated, which means it is old and you should not use it.
  • wait(), notify(), notifyAll() help threads coordinate. You meet them in the multithreading lessons.

The first three matter most. Each gets its own lesson next.

🖨️ The default toString prints a cryptic string

When you print an object, Java calls its toString to turn it into text. The default one looks strange.

class Student {
String name;
Student(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Student s = new Student("Alex");
System.out.println(s); // calls s.toString() automatically
}
}

You might hope this prints “Alex”. It does not.

Output

Student@1b6d3586
  • It prints the class name, then @, then the hash code in hexadecimal.
  • It knows nothing about your fields. So no name shows up.
  • The code after @ changes each time you run the program.
  • It is correct, but useless for a human.

That is why you usually write your own toString. We do that in the next lesson.

⚖️ The default equals checks identity, not contents

The default equals has a narrow idea of equal. It returns true only when both references point to the very same object in memory. This is called reference identity.

class Student {
String name;
Student(String name) {
this.name = name;
}
}
public class Main {
public static void main(String[] args) {
Student a = new Student("Alex");
Student b = new Student("Alex");
Student c = a;
System.out.println(a.equals(b)); // false
System.out.println(a.equals(c)); // true
}
}

Output

false
true
  • a and b look equal to a human, but equals says false. They are two separate objects.
  • a.equals(c) is true because c = a points at the same object.
  • Same contents do not count. Only the same object counts.

You usually want same-details to count as equal. That is why classes override equals. It has its own lesson next.

== and default equals do the same thing

For objects, == checks if two references point to the same object. The default equals does the same check. They only start to differ once you override equals to compare contents instead.

🔎 getClass tells you the real type

The getClass() method returns the actual class of your object at runtime. The common use is asking for the class name:

public class Main {
public static void main(String[] args) {
String text = "hello";
Integer number = 42;
int[] numbers = {1, 2, 3};
System.out.println(text.getClass().getName());
System.out.println(number.getClass().getName());
System.out.println(numbers.getClass().getName());
}
}

Output

java.lang.String
java.lang.Integer
[I
  • getName() gives the full name including the package.
  • The array prints [I, Java’s short code for “array of int”.
  • getClass always reports the true type, even through a general reference.
  • That makes it handy for logging and debugging.

⬆️ Any object can be stored in an Object reference

Because every class is a kind of Object, you can store any object in an Object variable. Moving an object into a more general reference type is called upcasting.

public class Main {
public static void main(String[] args) {
Object a = "a piece of text"; // a String is an Object
Object b = 100; // an Integer is an Object
Object c = new java.util.ArrayList<>(); // a list is an Object
System.out.println(a.getClass().getName());
System.out.println(b.getClass().getName());
System.out.println(c.getClass().getName());
}
}

Output

java.lang.String
java.lang.Integer
java.util.ArrayList
  • The variable type is Object, but getClass still reports the true type.
  • This is why the old ArrayList could hold anything before generics. It stored everything as Object.
  • It is also why a method that takes an Object parameter accepts any object.

🧠 Why you override toString, equals, and hashCode

The default behavior is very basic, so we often replace these three.

  • Override toString so printing shows useful details, not Student@1b6d3586.
  • Override equals so same-contents objects count as equal.
  • Override hashCode with equals, because HashMap and HashSet rely on both agreeing.

Each gets a full lesson next. Object gives you a working version for free. You improve it when the default falls short.

⚠️ Common Mistakes

A few Object misunderstandings trip up almost everyone.

  • Relying on the default toString. Printing an object and expecting readable details gives you the cryptic code instead.
System.out.println(student); // ❌ prints Student@1b6d3586
// ✅ override toString in Student to return the name and marks
  • Relying on the default equals. Comparing two objects with the same contents returns false unless you override equals.
new Student("Alex").equals(new Student("Alex")); // ❌ false by default
// ✅ override equals to compare the fields you care about
  • Forgetting that everything is an Object. Some learners think only “special” classes extend Object. Every class does, including your own and the ones in the library.
class Animal { } // ❌ thinking this has no parent
class Animal extends Object { } // ✅ this is what Java actually does for you
  • Overriding equals but not hashCode. These two must move together, or hash-based collections behave strangely. The hashCode lesson explains why.

✅ Best Practices

Good habits when working with Object and its methods.

  • Override toString on classes you print. It makes logs and debugging far easier than reading @1b6d3586 codes.
  • Override equals when “same contents” should mean equal. Do not depend on the default identity check for value-like classes.
  • Always override hashCode alongside equals. Keep the two in agreement so HashMap and HashSet work correctly.
  • Use getClass().getName() for clear logging. It is a quick, reliable way to see what an object really is.
  • Do not use finalize. It is deprecated and unreliable. Modern Java has better ways to release resources.

🧩 What You’ve Learned

Nicely done. Let’s recap the Object class.

  • Object (java.lang.Object) is the root of every class. If a class extends nothing, it extends Object automatically.
  • ✅ Every object inherits toString, equals, hashCode, getClass, clone, the deprecated finalize, and the thread methods wait / notify / notifyAll.
  • ✅ The default toString prints ClassName@hexhash, and the default equals checks reference identity, not contents.
  • getClass().getName() reports an object’s real type at runtime.
  • ✅ Any object can be upcast to an Object reference, which is why one general type can hold them all.
  • ✅ You usually override toString, equals, and hashCode to fit your own class.

Check Your Knowledge

Test what you learned. Pick an answer for each question, then click Check.

  1. 1

    What is the parent of a class that does not extend anything?

    Why: Any class without an explicit parent automatically extends java.lang.Object, the root of the hierarchy.

  2. 2

    What does the default toString return?

    Why: The inherited toString returns ClassName@hexhash, which is why you override it for readable output.

  3. 3

    What does the default equals compare?

    Why: By default, equals checks reference identity, the same as ==, so two separate objects are not equal even with identical fields.

  4. 4

    Why can any object be stored in an Object variable?

    Why: Since Object is the root of all classes, every object is also an Object, so upcasting to an Object reference always works.

🚀 What’s Next?

You now know that every object has a toString, and that the default version is unreadable. The natural next step is to fix that. Let’s learn how to write your own toString so your objects print clean, meaningful text.

Java toString() Method

Share & Connect