Skip to content
Ameen Abozaid
PublishedBEG-022-L03

Overriding methods

  • Lesson 3 of 7
  • 11 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

So far a subclass has only added things to its parent. Often that is not enough. A checking account lets the balance go below zero up to an overdraft limit, but the parent Account.withdraw forbids that. The child needs the same method name, with different behavior.

This lesson introduces overriding: writing a method in the subclass with the same name and parameters as one in the parent, so objects of the subclass use the new body. In the family tree picture, a child inherits the family recipe but cooks it its own way.

Why this matters: overriding is how you customize behavior in framework classes, and a single missing annotation, @Override, is behind many silent bugs. You will learn to type it every time.

This lesson builds on "Constructors and super" and uses super.method() from that lesson.

Learning Objectives

By the end of this lesson you will be able to:

  • Override an inherited instance method and tell overriding from adding a new method.
  • Use @Override and explain what it protects you from.
  • Call the parent's version from the override with super.method().
  • State the rules an override must follow (name, parameters, access, return type).
  • Explain why private methods and constructors cannot be overridden, and what happens with static methods.
  • Keep an override within the parent's promises.

Written Explanation

What overriding is

A subclass overrides an instance method when it declares a method with the same name, the same number and types of parameters, and the same return type as a method it inherited. The Oracle tutorial puts it this way: such a method in the subclass overrides the superclass method, and the subclass version is used for objects of the subclass.

java
class Animal {
    String speak() { return "..."; }
}

class Dog extends Animal {
    @Override
    String speak() { return "Woof"; }
}

A Dog object answers speak() with Woof; an Animal object still answers .... The parent is not changed.

Overriding is not adding

Change the name or the parameters and you have not overridden anything: you added a new method next to the inherited one. The tutorial notes that a method with the same name but a different parameter list is an overload, which neither hides nor overrides the original.

This is where the trap sits. Suppose you meant to override speak() but typed speek():

java
class Dog extends Animal {
    String speek() { return "Woof"; }
}

This compiles without complaint. new Dog().speak() prints ..., the parent's answer. The program is wrong, and nobody told you.

@Override: let the compiler check

Add the annotation and the compiler verifies your intention:

java
class Cat extends Animal {
    @Override
    String speek() { return "Meow"; }
}
text
Main.java:8: error: method does not override or implement a method from a supertype
    @Override
    ^
1 error

The tutorial says that @Override tells the compiler you intend to override a method, and that the compiler reports an error if no such method exists in a superclass. It costs ten characters and removes a whole class of silent bugs. It also tells the next reader: "this replaces something inherited." Make it a habit, as automatic as a semicolon.

Rules for an override

RuleDetail
Same signatureSame name and same parameter types, in the same order
Return typeSame type (a more specific subtype is also allowed; you will rarely need it)
AccessThe same or wider, never narrower
Instance onlyAn instance method cannot be overridden by a static method, or the other way round

The access rule is easy to break. If the parent's method is public and you write the override without a modifier, you narrowed it:

text
Main.java:7: error: run() in C cannot override run() in P
    void run() { }
         ^
  attempting to assign weaker access privileges; was public

Why the rule exists: anyone allowed to call the parent's method must also be allowed to call the child's version.

Calling the parent's version

Use super.method() inside an override to run the parent's code and then add to it, instead of repeating it:

java
class Tool {
    String name() { return "tool"; }
    String use() { return "using " + name(); }
}

class Hammer extends Tool {
    @Override
    String name() { return "hammer"; }

    @Override
    String use() { return super.use() + "!"; }
}

new Hammer().use() prints using hammer!. Two things happen. super.use() runs the code in Tool.use(). Inside that code the call name() is made on the current object, which is a Hammer, so the hammer's name() runs. That "the real object decides" behavior is the heart of the next module; for now just notice it.

What cannot be overridden

  • Constructors are not inherited, so there is nothing to override.
  • private methods are not inherited and a child cannot see them. If a child declares a method with the same name, it is a separate, unrelated method; adding @Override to it gives the "does not override" error.
  • static methods belong to the class, not to objects. A subclass static method with the same signature hides the parent's; it is not an override. The tutorial's example shows that which one runs depends on the class used in the call, not the object. Rule of thumb: do not declare a static method in a subclass with the same signature as a parent's static method.
  • final methods cannot be overridden. That is the next lesson.

Do not weaken the promise

An override must still keep the promises the parent made. The parent's withdraw promises: "the balance never goes below zero." If a child's override breaks that promise silently, code written against Account can break.

A legitimate change of rules must be part of the design. A checking account with an overdraft is a different kind of account with an allowed negative balance, which the bank explicitly decided. A savings account that charges a fee and accidentally pushes the balance below zero is a bug. Example 3 shows that bug.

Examples

Example 1: a typo that @Override catches (simple)

The Animal, Dog, Cat code above shows it. Without @Override, the Dog with speek() compiles and silently prints ... for speak(). With @Override on the Cat, the compiler stops you with method does not override or implement a method from a supertype.

Example 2: CheckingAccount with an overdraft (practical)

java
class Account {
    private final String owner;
    private double balance;

    Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    String getOwner() { return owner; }
    double getBalance() { return balance; }

    void deposit(double amount) {
        if (amount <= 0) {
            System.out.println("Deposit refused");
            return;
        }
        balance += amount;
    }

    boolean withdraw(double amount) {
        if (amount <= 0 || amount > balance) {
            System.out.println("Withdrawal refused");
            return false;
        }
        balance -= amount;
        return true;
    }

    protected void changeBalance(double delta) {
        balance += delta;
    }
}

class CheckingAccount extends Account {
    private final double overdraftLimit;

    CheckingAccount(String owner, double balance, double overdraftLimit) {
        super(owner, balance);
        this.overdraftLimit = overdraftLimit;
    }

    @Override
    boolean withdraw(double amount) {
        if (amount <= 0 || amount > getBalance() + overdraftLimit) {
            System.out.println("Withdrawal refused");
            return false;
        }
        changeBalance(-amount);
        return true;
    }
}

The parent offers a protected method changeBalance so that children can move the balance without the field itself being exposed. Using it in main:

java
Account plain = new Account("Sara", 100);
CheckingAccount chk = new CheckingAccount("Omar", 100, 200);
plain.withdraw(250);
chk.withdraw(250);
System.out.println(plain.getBalance());
System.out.println(chk.getBalance());
chk.withdraw(100);
System.out.println(chk.getBalance());

Output:

text
Withdrawal refused
100.0
-150.0
Withdrawal refused
-150.0

The plain account refuses 250. The checking account allows it (100 plus an overdraft of 200 covers it) and lands at -150.0. The next 100 would need more than the 50 of room left, so it is refused.

Example 3: a fee override that breaks the promise (realistic)

A savings account should charge a flat fee of 1 on each withdrawal. A first attempt reuses the parent through super:

java
class SavingsAccount extends Account {
    SavingsAccount(String owner, double balance) {
        super(owner, balance);
    }

    @Override
    boolean withdraw(double amount) {
        boolean ok = super.withdraw(amount);
        if (ok) {
            changeBalance(-1);   // flat fee
        }
        return ok;
    }
}

With a balance of 100, withdraw(100) is accepted by the parent (100 is not more than 100), then the fee is taken. The balance printed afterwards is:

text
-1.0

A savings account is now below zero, and no part of the code looked wrong. The override must check the total cost first:

java
class SavingsAccount extends Account {
    private static final double FEE = 1;

    SavingsAccount(String owner, double balance) {
        super(owner, balance);
    }

    @Override
    boolean withdraw(double amount) {
        if (amount + FEE > getBalance()) {
            System.out.println("Withdrawal refused");
            return false;
        }
        super.withdraw(amount);
        changeBalance(-FEE);
        return true;
    }
}

The same call now prints Withdrawal refused and the balance stays 100.0. Lesson: reusing the parent with super does not make an override safe. Think about what the parent promises.

Backend Insight

Overriding is the main way backend frameworks let you customize behavior: a base class defines a method that does a standard job, and you override it to change one step. Two professional habits follow:

  • Always use @Override. In a large codebase a base class method may be renamed. With the annotation, the compiler flags every override that no longer matches; without it, your customization silently stops being called.
  • Treat an override as a contract. Code written against the parent type trusts the parent's rules. A subtle override that breaks them (like the fee bug) can be hard to find, because the failure appears far away from the override.

Common Mistakes

Mistake 1: Omitting @Override

A typo or a changed parameter type creates a new method and the intended override never runs. Always add the annotation.

Mistake 2: Changing the parameters and calling it an override

withdraw(int amount) does not override withdraw(double amount). It is an overload. With @Override the compiler would refuse it.

Mistake 3: Narrowing the access

An override declared with less access than the parent's method is a compile error. Keep the same modifier or widen it.

Mistake 4: Trying to override a private or static method

A private method is invisible to the child, and a static method is hidden, not overridden. Neither gives you the replace-the-behavior effect you wanted.

Mistake 5: Calling the parent's code incorrectly

Writing withdraw(amount) inside the override of withdraw calls itself and recurses forever. To call the parent's version, write super.withdraw(amount).

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. What three things must match for a subclass method to override an inherited instance method?

Show solution A1

A1. The name, the parameter types (number, types, order), and the return type (or a compatible subtype). Access must also be the same or wider.

A2. What does @Override do, and what happens at compile time if there is no matching parent method?

Show solution A2

A2. It tells the compiler that the method is meant to override a parent method. If none matches, the compiler reports an error: method does not override or implement a method from a supertype.

A3. The parent method is public void run(). Which of these overrides is legal: void run() or public void run()?

Show solution A3

A3. Only public void run(). Dropping public narrows the access and is a compile error.

Level B: Application

B1. Shape has String name() { return "shape"; }. Write Circle extends Shape that overrides name() to return "circle", then a second override of a describe() method that uses super.describe() plus " (round)", given that Shape.describe() returns "I am a " + name(). What does new Circle().describe() return?

Show solution B1

B1.

java
class Circle extends Shape {
    @Override
    String name() { return "circle"; }

    @Override
    String describe() { return super.describe() + " (round)"; }
}

super.describe() returns "I am a " + name(). Since the object is a Circle, name() gives circle, so the result is I am a circle (round).

B2. Find the bug and fix it: the author wants Employee.bonus() to return a double and the manager version to add 500.

java
class Employee { double bonus() { return 100; } }
class Manager extends Employee {
    double bonus() { return bonus() + 500; }
}
Show solution B2

B2. bonus() inside Manager.bonus() calls itself forever (it ends with a stack overflow). Use super.bonus() + 500 and add @Override: @Override double bonus() { return super.bonus() + 500; }. A manager's bonus is then 600.

B3. Parent has private void log(). Child declares @Override void log() { }. Predict the outcome.

Show solution B3

B3. A compile error: method does not override or implement a method from a supertype, because a private method is not inherited and so cannot be overridden. Without @Override it would compile as an unrelated method.

Level C: Challenge

C1. A PremiumAccount extends Account overrides withdraw to allow withdrawing up to the balance plus 50 without recording the extra 50 as overdraft anywhere. Describe two problems with this override relative to the parent's promise.

Show solution C1

C1. (1) The parent promises the balance never goes below zero, and the override lets it go 50 below with no rule limiting or recording it, so the balance can silently become negative. (2) Code that trusts Account (for example reports that assume a non-negative balance) can now see a value it was never designed for, so a failure can appear far from the override. If a bank really wants an overdraft, it should be an explicit, bounded, named feature as in CheckingAccount.

C2. Why does the compiler require an override to be at least as accessible as the original? Give a short example of what could go wrong otherwise.

Show solution C2

C2. Code written against the parent may call the method because it is accessible to it. If a child narrowed the access, that same call on a child object would suddenly be forbidden. For example, a public run() replaced with a private run() would let the call compile against the parent type but be illegal for a child instance, so the compiler forbids narrowing.

Mini Challenge

Take the Account and CheckingAccount from Example 2. Add a describe() method to Account that returns owner + ": " + balance. In CheckingAccount, override it to return super.describe() + " (overdraft " + overdraftLimit + ")". Then deliberately misspell the override as descibe() with @Override and read the compiler message. Finally, remove @Override and notice that the program compiles, but your new text never appears.

Check your work: you saw both outcomes of the typo: a compile error with @Override, and silently wrong output without it.

Summary & Next Lesson

Key takeaways

  • Overriding means the same signature, a new body, in a subclass.
  • Always write @Override so the compiler catches typos and signature mismatches.
  • An override cannot narrow access; private methods and constructors cannot be overridden; static methods are hidden, not overridden.
  • super.method() calls the parent's version.
  • An override must keep the parent's promises, and reusing super does not guarantee that.

Next lesson

The next lesson, "final classes and methods", shows how a class author forbids overriding or extending when it would be unsafe.

Self-check

  1. What is the difference between overriding and overloading?
  2. What does @Override protect you from?
  3. How do you call the parent's version of a method from the override?
  4. Can a private method be overridden?
  5. Why did the fee override in Example 3 produce a negative balance?
Show answers
  1. Overriding replaces an inherited method with the same signature. Overloading adds a method with the same name and different parameters.
  2. Mistyped names or wrong parameters that silently create a new method.
  3. With super.methodName(...).
  4. No. It is not inherited.
  5. It let the parent's check pass first and then subtracted the fee without checking that the balance could cover it.

References

  • Oracle, Java Tutorials, Overriding and Hiding Methods. Supports the override rules, covariant return types, access that cannot be narrower, the @Override annotation and its compile-time error, and static method hiding.
  • The compiler messages and outputs were produced by compiling and running the code with JDK 21. The "what Java does with a private method of the same name" statement was checked by compiling it; the account examples and fee scenario are this course's own.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.