Java Method Overriding
Table of Contents + −
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 soundWoof woofSame 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 animalI am a dog tooSo 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 woofMeowSome generic animal soundOne loop, one method call, three different results.
- The reference type did not decide. The real object did.
- No
ifchecks 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
trueTwo 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.
staticmethods 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 readabletoString()makes debugging far easier. - Override
equals()andhashCode()together. Then the object behaves correctly in collections likeHashSetandHashMap. - Mark methods
finalwhen they must not change.finalblocks 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
@Overrideannotation 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, orprivatemethods. - ✅
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()andequals()fromObjectis 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
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
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
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
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.