Java Method Overriding

In the last lesson you learned about Java types of inheritance. So a child can reuse a parent’s methods, but reuse alone is not enough. A Dog inherits makeSound() from Animal, yet a dog should bark, not make a generic sound. Method overriding lets the child keep the method name but give it a brand new body.

🤔 What is method overriding?

Method overriding is when a subclass writes its own version of a method it already inherited.

  • The new version has the same name and the same parameters. That is the same signature.
  • The signature is the method name plus its parameter types. Java uses it to match the two methods.
  • When the child’s signature matches the parent’s, the child’s version replaces the parent’s.

Here is the smallest example.

class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
@Override
void makeSound() { // ✅ same name, same parameters
System.out.println("Woof woof");
}
}

Dog inherited makeSound() but declares its own with the same signature. So a Dog no longer uses the generic sound. Run it.

public class Main {
public static void main(String[] args) {
Animal a = new Animal();
a.makeSound(); // uses Animal's version
Dog d = new Dog();
d.makeSound(); // uses Dog's overriding version
}
}

Output

Some generic animal sound
Woof woof

Same method name, two different results. The child kept the name but changed the behavior.

🏷️ The @Override annotation

@Override tells Java you mean this method to replace a parent method.

  • Java then checks that a matching parent method really exists.
  • If it does not, the code refuses to compile.
  • It is optional, but always write it. It catches a sneaky class of bugs.

Watch what happens with a typo and no annotation.

class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
void makesound() { // ❌ lowercase 's', a typo
System.out.println("Woof woof");
}
}

You meant makeSound but typed makesound. Java sees no matching parent, so it treats this as a brand new method. No error. Your Dog quietly still uses the generic sound. Now add the annotation.

class Dog extends Animal {
@Override
void makesound() { // ❌ won't compile now
System.out.println("Woof woof");
}
}

Output

error: method does not override or implement a method from a supertype
void makesound() {
^

The compiler caught the typo the moment you built. @Override turns a silent bug into a loud compile error.

📋 The rules of overriding

Overriding only works when you follow Java’s rules. Break one and the method fails to compile or stops being an override.

Rule What it means
Same name and parameters The signature must match the parent’s method exactly
Same or covariant return type Return the same type, or a subclass of the parent’s return type
Access not more restrictive You may widen access (protected to public), never narrow it
Cannot override final A method marked final is locked and cannot be changed by a child
Cannot override static static methods belong to the class, not the object, so they are not overridden
Cannot override private private methods are not inherited, so there is nothing to override

The covariant return type rule is simple. If the parent returns an Animal, the child’s override may return a Dog, because a Dog is an Animal. The child can be more specific.

class Animal {
Animal reproduce() { // returns an Animal
return new Animal();
}
}
class Dog extends Animal {
@Override
Dog reproduce() { // ✅ covariant: Dog is an Animal
return new Dog();
}
}

🆙 Calling the parent version with super

Sometimes you want the parent’s work first, then your own on top. The super.method() call reaches the parent’s version from inside your override.

class Animal {
void describe() {
System.out.println("I am an animal");
}
}
class Dog extends Animal {
@Override
void describe() {
super.describe(); // run the parent's line first
System.out.println("I am a dog too"); // then add my own
}
}

Now run it.

public class Main {
public static void main(String[] args) {
new Dog().describe();
}
}

Output

I am an animal
I am a dog too

So super lets you extend the parent’s behavior instead of replacing it completely.

🎯 Runtime dispatch: the real object wins

This is the most important part of the lesson.

  • When you call an overridden method, Java looks at the actual object, not the reference variable’s type.
  • Java picks the version while the program runs.
  • This is called runtime dispatch, or dynamic dispatch. It is the engine behind runtime polymorphism.

Here an Animal reference points to a Dog object.

class Animal {
void makeSound() {
System.out.println("Some generic animal sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Woof woof");
}
}
class Cat extends Animal {
@Override
void makeSound() {
System.out.println("Meow");
}
}

Now hold different real objects in Animal variables and call the same method on each.

public class Main {
public static void main(String[] args) {
Animal[] animals = { new Dog(), new Cat(), new Animal() };
for (Animal animal : animals) {
animal.makeSound(); // each object runs its own version
}
}
}

Output

Woof woof
Meow
Some generic animal sound

One loop, one method call, three different results.

  • The reference type did not decide. The real object did.
  • No if checks for the type anywhere.
  • This is dynamic dispatch, the heart of runtime polymorphism.

🔀 Overriding vs overloading

These two words look alike and get mixed up.

  • Overriding replaces a parent method in a child class. Same parameters.
  • Overloading is two methods with the same name but different parameters in one class.

Here they are side by side.

Aspect Overriding Overloading
Where In a child class In the same class
Parameters Must be the same Must be different
Method name Same Same
Decided At runtime, by the object At compile time, by the arguments
@Override fits Yes No

Here is one class showing both at once.

class Dog extends Animal {
@Override
void makeSound() { // overriding: same signature as parent
System.out.println("Woof");
}
void makeSound(int times) { // overloading: different parameters
for (int i = 0; i < times; i++) {
System.out.println("Woof");
}
}
}

The first makeSound() overrides the parent. The second makeSound(int times) is overloading, a separate method that shares the name. The @Override only sits on the real override.

🧰 Overriding toString and equals

Every class secretly extends Object, so every class inherits toString() and equals().

  • Their defaults are not useful, so overriding them is very common.
  • toString() prints an ugly class name plus a memory code by default. Override it to print something readable.

Here is a readable toString().

class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}

Now when you print a Point, Java calls your toString() for you.

public class Main {
public static void main(String[] args) {
Point p = new Point(3, 5);
System.out.println(p); // calls toString() for us
}
}

Output

Point(3, 5)

Much nicer than a memory code. The equals() method is the other common one.

  • By default it returns true only when two variables point to the exact same object.
  • Often you want two points with the same x and y to count as equal.

Override it like this.

class Point {
int x;
int y;
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)) return false;
Point other = (Point) o;
return this.x == other.x && this.y == other.y;
}
}

Test it with two separate objects that hold the same values.

public class Main {
public static void main(String[] args) {
Point a = new Point(3, 5);
Point b = new Point(3, 5);
System.out.println(a.equals(b)); // now compares values
}
}

Output

true

Two different objects, but equals() says they match because their values match. Notice equals takes an Object parameter, exactly like the parent’s. Write equals(Point o) instead and that is overloading, not overriding, so the default still runs. The signature really matters.

⚠️ Common Mistakes

A few overriding slip-ups show up over and over:

  • Wrong signature, so you overload instead of override. If the parameters differ even slightly, Java treats it as a new method and the parent’s version still runs.
class Dog extends Animal {
void makeSound(String tone) { } // ❌ different params: this is overloading
@Override
void makeSound() { } // ✅ matches parent exactly
}
  • Skipping @Override. Without it, a typo in the name creates a silent new method and no error appears.
class Dog extends Animal {
void makeSund() { } // ❌ typo, silently NOT an override
@Override
void makeSound() { } // ✅ compiler verifies the match
}
  • Trying to override a static method. static methods belong to the class, not the object, so they are never overridden. Writing the same static method in a child just hides the parent’s one, which is confusing.
class Animal {
static void info() { }
}
class Dog extends Animal {
@Override
static void info() { } // ❌ won't compile, static can't be overridden
}

✅ Best Practices

Habits that keep your overrides clean:

  • Always write @Override. It catches signature and typo mistakes the moment you build.
  • Use super.method() to reuse parent logic. Do the parent’s part, then add your own.
  • Keep the override consistent with the parent’s promise. If the parent returns a sound, your override should still return a sound, just a different one.
  • Override toString() on your value classes. A readable toString() makes debugging far easier.
  • Override equals() and hashCode() together. Then the object behaves correctly in collections like HashSet and HashMap.
  • Mark methods final when they must not change. final blocks overriding and makes that intent clear.

🧩 What You’ve Learned

Nicely done. That was a lot, so let’s recap method overriding.

  • Overriding is when a subclass redefines an inherited method with the same signature but a new body.
  • ✅ The @Override annotation makes the compiler check the override, turning silent bugs into clear errors.
  • ✅ The rules: same name and params, same or covariant return type, access not more restrictive, and you cannot override final, static, or private methods.
  • super.method() calls the parent’s version so you can reuse and extend it.
  • Runtime dispatch means the actual object decides which version runs, even through a parent reference like Animal a = new Dog().
  • Overriding needs the same parameters in a child; overloading needs different parameters in the same class.
  • ✅ Overriding toString() and equals() from Object is one of the most common real-world uses.

Check Your Knowledge

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

  1. 1

    What must match between a parent method and its override in the child?

    Why: An override must have the same name and the same parameters as the parent method.

  2. 2

    In Animal a = new Dog(); a.makeSound(); whose makeSound runs if Dog overrides it?

    Why: Runtime dispatch picks the method based on the actual object, which is a Dog, not the reference type.

  3. 3

    Which method CANNOT be overridden?

    Why: static methods belong to the class, not the object, so they are not overridden. final and private also cannot be overridden.

  4. 4

    What is the difference between overriding and overloading?

    Why: Overriding keeps the same signature in a child class; overloading uses different parameters in the same class.

🚀 What’s Next?

Overriding is the engine, but you have only seen its first sparks. Next you will see how overriding, references, and one method name combine into one of the most powerful ideas in object-oriented programming. One interface, many forms.

Java Polymorphism

Share & Connect