Method Overriding and the super Keyword

8 min read

Inheritance lets a child class add methods. Method overriding lets a child class replace a parent's method with its own version — same name, same parameters, different behaviour. This is the OOP feature that makes a generic BasePage.waitForLoad() work on a LoginPage (waits for the login form) and on a DashboardPage (waits for the widgets to render). Each page knows how to wait for itself; the test framework just calls page.waitForLoad() and lets polymorphism dispatch to the right version. This lesson covers the rules of overriding, the @Override annotation that catches typos, and super.method() for "extend the parent, don't replace it entirely."

What overriding looks like

A parent with a generic implementation:

public class BasePage {
    protected String url;
    public BasePage(String url) { this.url = url; }
 
    public void waitForLoad() {
        System.out.println("Generic wait — checking title is non-empty");
    }
}

A child that supplies its own version:

public class DashboardPage extends BasePage {
    public DashboardPage() { super("https://staging.myapp.com/dashboard"); }
 
    @Override
    public void waitForLoad() {
        System.out.println("Dashboard wait — checking #widget is visible");
    }
}

Calling code:

public class WaitDemo {
    public static void main(String[] args) {
        BasePage generic = new BasePage("https://staging.myapp.com/about");
        DashboardPage dash = new DashboardPage();
 
        generic.waitForLoad();
        dash.waitForLoad();
    }
}

Output:

Generic wait — checking title is non-empty
Dashboard wait — checking #widget is visible

The two calls look identical at the call site (x.waitForLoad()), but Java picks the right version based on the actual type of the object — not the type of the variable. That decision is made at runtime, which is what makes this polymorphism (lesson 3 of chapter 5 will dig into the term properly).

The @Override annotation — always use it

@Override on a method declaration tells the compiler: I intend to override a method from the parent. If there's no matching parent method, the compiler throws an error and you find your typo immediately:

@Override
public void waitFroLoad() {       // typo!
    ...
}
error: method does not override or implement a method from a supertype

Without @Override, this typo would silently create a brand-new method on the child — waitFroLoad — that no one ever calls. The parent's waitForLoad would still run. The bug is invisible until the test relies on the override and gets the wrong behaviour.

@Override is technically optional (the override works either way) but every Java style guide requires it. Treat it as mandatory. IntelliJ adds it automatically when you generate or override a method.

Rules — what counts as overriding

For Java to consider a child method an override:

  • Same name.
  • Same parameter types in the same order.
  • Same return type (or a subtype of the return type — called a covariant return).
  • Equal or more permissive access modifier. Parent public → child public ✅. Parent public → child protected ❌.
  • Cannot throw broader checked exceptions than the parent declared (we'll meet checked exceptions in chapter 7).

If any of these don't match, the child method isn't overriding — it's a different method, and @Override will make the compiler tell you so.

Overriding vs overloading — keep them straight

These two words sound alike and mean opposite things. Internalise this once:

Overriding vs Overloading

Overriding — child REPLACES parent's method

  • Same name, SAME parameter types

  • Method is declared on parent and child

  • Annotated with @Override on the child

  • Runtime decides which version runs based on the object's actual type

  • QA: BasePage.waitForLoad() → LoginPage's override of waitForLoad()

Overloading — multiple methods, SAME class

  • Same name, DIFFERENT parameter types or counts

  • All overloads usually live on the same class

  • Compile time decides which version runs based on argument types

  • QA: findElement(String css) and findElement(By locator)

  • Has nothing to do with inheritance

The cheat sheet: overriding crosses the parent-child boundary; overloading stays within one class. The compiler resolves overloads at compile time; the JVM resolves overrides at runtime. They are fully independent features that happen to sound similar.

super.method() — extend, don't replace

Sometimes the child wants to add to the parent's behaviour, not replace it. super.method(...) calls the parent's version from inside the child:

public class BasePage {
    public void waitForLoad() {
        System.out.println("• generic check: title non-empty");
    }
}
 
public class DashboardPage extends BasePage {
    @Override
    public void waitForLoad() {
        super.waitForLoad();                          // run the parent first
        System.out.println("• dashboard check: #widget visible");
    }
}

Calling new DashboardPage().waitForLoad() prints both lines:

• generic check: title non-empty
• dashboard check: #widget visible

This pattern — do the parent's work, then add my own — is everywhere in test framework hierarchies. BasePage.setUp() clears cookies; subclass setUp() calls super.setUp() then logs in. The order matters: super.setUp() first, page-specific work after.

You can also call the parent after doing your own work, or skip super.method() entirely if you genuinely want to replace the parent's behaviour wholesale. The choice is yours; super.method() is a tool, not a requirement.

A real example — failure-aware screenshot

A BasePage has a generic screenshot method. A FailurePage overrides it to include the error in the filename:

public class BasePage {
    protected String url;
    public BasePage(String url) { this.url = url; }
 
    public String screenshot() {
        return "screenshot-of-" + slug(url) + ".png";
    }
 
    protected String slug(String s) {
        return s.replace("https://", "").replace("/", "_");
    }
}
 
public class FailurePage extends BasePage {
    private final String errorCode;
 
    public FailurePage(String url, String errorCode) {
        super(url);
        this.errorCode = errorCode;
    }
 
    @Override
    public String screenshot() {
        String base = super.screenshot();             // reuse parent's logic
        return base.replace(".png", "-" + errorCode + ".png");
    }
}
 
public class ScreenshotDemo {
    public static void main(String[] args) {
        BasePage about = new BasePage("https://staging.myapp.com/about");
        FailurePage fail = new FailurePage("https://staging.myapp.com/checkout", "ERR_500");
 
        System.out.println(about.screenshot());
        System.out.println(fail.screenshot());
    }
}

Output:

screenshot-of-staging.myapp.com_about.png
screenshot-of-staging.myapp.com_checkout-ERR_500.png

FailurePage.screenshot() calls super.screenshot() to get the standard filename, then appends the error code. The slug logic only lives on BasePage; the override layers on top. If you later change the slug rules in the parent, every subclass picks up the change automatically — that's the leverage of super.method().

Polymorphism in one line

Here's why this matters for test code:

BasePage[] pages = {
    new LoginPage(),
    new DashboardPage(),
    new FailurePage("https://staging.myapp.com/checkout", "ERR_500")
};
 
for (BasePage page : pages) {
    page.navigate();
    page.waitForLoad();        // ← each page's own override runs
    System.out.println(page.screenshot());
}

The variable type is BasePage[], so the compiler only knows it's holding BasePage references. At runtime, Java looks at each object's actual type and dispatches to the right waitForLoad and screenshot. Three different behaviours, one loop, no if/switch on type. That is the whole point of overriding, and we'll come back to it as polymorphism in chapter 5.3.

⚠️ Common mistakes

  • Skipping @Override and silently creating a sibling method. Without the annotation, a typo (waitFroLoad) compiles fine and creates a separate method that nobody calls. The parent's waitForLoad keeps running, masquerading as the override. Use @Override on every override.
  • Confusing overriding with overloading. Adding a findElement(WebElement parent, String css) to a child class next to an inherited findElement(String css) is overloading, not overriding. Both methods exist. Useful sometimes, surprising when you expected a replacement.
  • Forgetting super.method() and replacing setup that the parent needed. If BasePage.setUp() clears cookies, and your child override forgets super.setUp(), every test starts with stale cookies and you spend a day chasing flakes. When the parent does meaningful work, call super unless you're sure you don't want it.

🎯 Practice task

Override behaviour in a small page hierarchy. 25-30 minutes.

  1. Open the BasePage / LoginPage / DashboardPage files from lesson 4 (or recreate them).
  2. On BasePage, define public void waitForLoad() { System.out.println("[base] generic wait for " + url); }.
  3. On LoginPage, override it: print [login] waiting for #email-input. Do not call super.waitForLoad(). (Login pages typically don't need the generic title check.)
  4. On DashboardPage, override it: call super.waitForLoad() first, then print [dashboard] waiting for #widget. (Dashboards do want the generic check + the widget check.)
  5. In a Demo class, declare BasePage[] pages = { new LoginPage(), new DashboardPage(), new BasePage("https://staging.myapp.com/about") };. Loop and call page.waitForLoad() on each. Confirm each prints the right output — proof that the JVM is picking the override based on the actual object type.
  6. Add @Override to both child overrides. Now deliberately misspell one as waitForLaod — read the compile error and confirm that @Override caught it.
  7. Stretch: override screenshot() on LoginPage to call super.screenshot() then append -login. Run it and inspect the filename. Then accidentally change the parameter list on the override — say, take a String name argument — and watch the override silently turn into an overload (a brand-new method with two arguments). @Override will fail to compile, which is exactly the safety net we wanted.

You now have the full Chapter 4 toolkit: classes, constructors, encapsulation, inheritance, and overriding. Chapter 5 takes the next step into OOP — abstract classes, interfaces, and polymorphism — the foundations of every test framework you'll write.

// tip to track lessons you complete and pick up where you left off across devices.