Marked Override But Does Not Override

7 min read

Marked Override That Does Not Actually Override: Understanding the Pitfalls of @Override in Java

When you see the @Override annotation in a Java source file, it signals that a method intends to replace a counterpart defined in a superclass or implemented in an interface. But the annotation is meant to improve code clarity and help the compiler catch potential errors. Still, there are situations where a method is marked with @Override yet does not truly override its parent implementation. This mismatch can lead to subtle bugs, unexpected behavior, and maintenance headaches. In this article we’ll explore why a marked override may fail, examine common causes, and provide practical tips to ensure your overrides work as intended.

What @Override Actually Does

The @Override annotation was introduced in Java 5 (JDK 1.5). It is a compile‑time marker that tells the Java compiler:

  • “I intend this method to replace a method from a supertype.”
  • “If no such method exists, the compiler should raise an error.”

Because the annotation is processed at compile time, it does not guarantee runtime behavior. The method will be called according to normal inheritance rules, but if the signature or access rights do not match a method in the superclass, the override is ineffective.

How Override Normally Works

  1. Method Signature Matching – The overriding method must have the same parameter list (order and types) and return type (or a covariant return type).
  2. Access Modifier Compatibility – The overriding method cannot be more restrictive; public can override protected, but private cannot override anything.
  3. Exception Compatibility – Overridden methods may throw fewer or narrower checked exceptions.
  4. Static, Final, and Private Methods – These cannot be overridden in the traditional sense; they are resolved at compile time.

If any of these rules are broken, the method is not considered an override, even when annotated with @Override.

When a Marked Override Fails to Override

Below are the most frequent scenarios where a developer adds @Override but the method does not actually replace the superclass version.

1. Signature Mismatch

class Animal {
    void makeSound() { System.out.println("Generic sound"); }
}

class Dog extends Animal {
    @Override
    void makeSound(String tone) {          // ❌ Wrong signature
        System.out.println("Bark: " + tone);
    }
}

Because the parameter list differs (void makeSound() vs. void makeSound(String tone)), the compiler still allows the code (the annotation is ignored) and the original makeSound() is called at runtime.

2. Return‑Type Incompatibility

class Shape {
    Number getArea() { return 0; }
}

class Circle extends Shape {
    @Override
    Double getArea() { return 3.14 * radius * radius; } // ❌ Covariant return is okay, but raw types differ
}

If the superclass returns Number and the subclass returns Double, the compiler may warn about incompatible return types, especially with raw types. In practice, g. Covariant returns are allowed only when the subclass return type is a subtype of the superclass return type (e., Number → Double is okay, but Object → String is not).

Worth pausing on this one.

3. Access‑Modifier Restriction

class Parent {
    protected void secret() {}
}

class Child extends Parent {
    @Override
    private void secret() {} // ❌ More restrictive – cannot override
}

A private method cannot override a protected one; the child simply introduces a new method.

4. Throwing More Checked Exceptions

class Service {
    void connect() throws IOException {}
}

class MyService extends Service {
    @Override
    void connect() throws SQLException {} // ❌ Cannot add a new checked exception
}

Java disallows adding a checked exception that isn’t a subclass of the original exception.

5. Overriding final or static Methods

class Base {
    final void freeze() {}
}

class Derived extends Base {
    @Override
    void freeze() {} // ❌ Cannot override a final method
}

The @Override annotation will cause a compile‑time error because the method cannot be overridden Surprisingly effective..

6. Incorrect Use of @Override with Interfaces

When a class implements an interface, each default method must be overridden with the exact signature. A common mistake is forgetting to match the default method’s parameters.

interface Drawable {
    default void draw(int x) {}
}

class Circle implements Drawable {
    @Override
    void draw() {} // ❌ Wrong signature – does not override default method
}

Why Developers Still Use @Override in These Cases

  • Intent Documentation – The annotation serves as a clear comment for future maintainers.
  • Compiler Safety – In most correct usages, @Override catches signature errors early.
  • Legacy Code – Refactoring may leave behind stale annotations that no longer match the superclass.

Even when an override fails, the code compiles, which can hide the problem until runtime Most people skip this — try not to..

Benefits of Proper Override Usage

  • Compile‑Time Guarantees – The compiler will flag missing overrides, protecting you from accidental method addition.
  • Improved Readability – Developers can instantly see which methods are intended to replace superclass behavior.
  • Better Refactoring Support – IDEs can deal with to overridden methods more accurately when annotations are correct.

Best Practices to Ensure Your Overrides Work

  1. Always Use @Override – Even if you think the method is correct, the annotation acts as a safety net.
  2. Check Signatures Carefully – Use your IDE to highlight overridden methods; most IDEs will underline mismatched signatures.
  3. Respect Access Modifiers – The overriding method must be less restrictive, never more.
  4. Maintain Exception Contracts – Throw only subclasses of the original checked exceptions.
  5. Avoid Overriding final or static Methods – If you need different behavior, consider composition or delegation.
    6

6. Avoid Overriding final or static Methods
When a method is marked final, the JVM guarantees that its implementation cannot be changed by a subclass. Attempting to override it simply confuses the compiler and the reader – the annotation will highlight the mismatch, but the real issue is design. If you need alternative behavior for a final method, consider delegating to a helper object or extracting the logic into a separate utility class. The same reasoning applies to static methods: they belong to the class, not to its inheritance hierarchy, so overriding them defeats the purpose of static polymorphism.

class Util {
    public static void log(String msg) {
        System.out.println("[LOG] " + msg);
    }
}

class Service {
    public void process() {
        Util.log("Processing started");
        // … business logic …
    }
}

Here Util.log is never overridden; instead, Service uses composition to benefit from the logging utility Small thing, real impact. Turns out it matters..


7. Mind the Generic Type Bounds
If the overridden method is generic, the type parameters must be either identical or more restrictive. A common slip is to widen a bound unintentionally, which the compiler will flag when @Override is present Surprisingly effective..

class Base {
    public  void transform(U input) { /* … */ }
}

class Derived extends Base {
    // This compiles because the bound is relaxed (U extends String)
    @Override
    public  void transform(U input) { } // ❌ Wrong: loses the bound
}

The annotation warns you that the signature no longer matches the parent’s generic contract Worth keeping that in mind..


8. Preserve the synchronized Contract
A method that is synchronized in the superclass can only be overridden by a method that is at least as synchronized. Downgrading synchronization can introduce race conditions, while upgrading it is generally safe but may affect performance.

class CriticalSection {
    public synchronized void access() { /* … */ }
}

class SubClass extends CriticalSection {
    @Override
    public synchronized void access() { /* … */ } // ✅ Keeps the contract
}

9. Avoid Changing the Return Type (except for covariant returns)
In Java, the return type of an overriding method may be a subtype of the original (covariant return). Anything else—wider type, raw type, or void—breaks the Liskov Substitution Principle and will be caught by the @Override check And it works..

class Shape {
    public Circle draw() { return new Circle(); }
}

class Square extends Shape {
    @Override
    public Circle draw() { return new Circle(); } // ✅ Covariant, OK
}

If you attempted public Shape draw() in Square, the compiler would complain because the return type is not a subtype.


Conclusion

The @Override annotation is far more than a stylistic flourish; it is a compile‑time guardian that enforces the rules of inheritance, protects against accidental signature drift, and documents intent for anyone reading the code later. By rigorously applying the best practices outlined above—respecting exception contracts, avoiding final or static overrides, honoring generic bounds, preserving synchronization, and using covariant returns—you see to it that your class hierarchy remains reliable, maintainable, and truly substitutable. When used correctly, @Override turns potential runtime bugs into immediate, actionable compile errors, allowing you to build Java systems that are both safe and expressive.

Just Added

Hot Topics

See Where It Goes

People Also Read

Thank you for reading about Marked Override But Does Not Override. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home