Java Overriding Rules: Signatures, Access, and Exceptions
java overriding rules: Learn the exact rules for overriding methods in Java: signature matching, access levels, exceptions, static/final methods, and covariant return...
java overriding rules requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The Core Rule: Same Signature, Same Return Type
In Java, overriding a method means providing a new implementation in a subclass for a method already defined in a superclass. The fundamental rule is that the overriding method must have the same name, the same parameter list, and a return type that is either identical or a subtype of the original return type (covariant return). The parameter types must match exactly; the compiler does not treat Integer and int as the same when they appear in a method signature.
class Animal { public String speak() { return "Some sound"; } } class Dog extends Animal { @Override public String speak() { return "Bark"; } }
Here Dog.speak() overrides Animal.speak() because the name, parameters (none), and return type (String) match exactly. If you change the parameter list, you are overloading, not overriding. That is a common source of confusion: a method with the same name but different parameters is a separate method, and the @Override annotation will cause a compile error if the signature does not match a superclass method.
Access Modifiers: Can You Reduce Visibility?
An overriding method cannot be more restrictive than the method it overrides. If the superclass method is public, the subclass method must be public. If it is protected, the subclass method can be protected or public, but not package-private or private. This rule exists because an overriding method must be usable anywhere the original method was usable; otherwise, polymorphism would break.
class Base { protected void display() { } } class Child extends Base { // This will not compile: // void display() { } // package-private is more restrictive // This is correct: @Override protected void display() { } }
You may widen access (from protected to public) but never narrow it. This rule preserves the contract that a subclass instance can be substituted for a superclass instance without breaking callers that rely on the original access level.
Exceptions: What Overriding Methods May Throw
An overriding method may throw the same checked exceptions as the original method, or subclasses of those exceptions, but it may not throw broader checked exceptions. It may also choose to throw no checked exceptions at all. Unchecked exceptions (subclasses of RuntimeException) are not subject to this restriction because they are not part of the checked exception contract.
import java.io.IOException; class Parent { public void read() throws IOException { } } class Child extends Parent { @Override public void read() throws IOException { } // same exception // This would not compile: // public void read() throws Exception { } // broader }
The rule ensures that code calling the method through the superclass type only needs to handle exceptions declared by the superclass. If a subclass could throw a broader checked exception, callers would not be prepared for it, breaking the substitution principle.
Static, Final, and Private Methods: What Cannot Be Overridden
Static methods belong to the class, not to an instance, so they are not overridden. If you declare a static method with the same signature in a subclass, it is method hiding, not overriding. The method that runs depends on the reference type, not the object type. Similarly, final methods cannot be overridden; the compiler enforces this to prevent subclasses from altering behavior that the superclass author intended to be fixed.
Private methods are not inherited, so they cannot be overridden. A subclass can declare a method with the same name and parameters, but it is a new method, not an override. This is a frequent source of confusion when a subclass defines a method that happens to match a private method in the superclass.
class Base { private void secret() { } public void call() { secret(); } } class Derived extends Base { private void secret() { } // Not an override; just a new method }
When call() is invoked on a Derived instance, it executes Base.secret(), not Derived.secret(), because the private method is not part of the virtual dispatch.
Covariant Return Types and Generic Methods
Java allows an overriding method to return a subtype of the original return type. This is called a covariant return type and is useful when you want to return a more specific type from the subclass without forcing callers to cast.
class Shape { public Shape copy() { return new Shape(); } } class Circle extends Shape { @Override public Circle copy() { return new Circle(); } }
Here Circle.copy() returns Circle, which is a subtype of Shape. This is valid because any caller expecting a Shape will still receive a Circle, which is a Shape. The same principle applies to generic methods, but with an important caveat: due to type erasure, the signature after erasure must be compatible. For example, a method returning List<String> cannot be overridden by a method returning List<Integer> because both erase to List. The compiler will reject such a declaration as a name clash.
The Role of @Override and Common Pitfalls
The @Override annotation is not required, but it is strongly recommended. It tells the compiler to verify that the method actually overrides a superclass method. If the signature does not match, you get a compile error instead of silently creating an overload. This catches mistakes early.
A common pitfall is accidentally overloading instead of overriding by changing the parameter type. For example:
class Parent { public void process(String value) { } } class Child extends Parent { // This is an overload, not an override: public void process(Object value) { } }
Because the parameter types differ, Child now has two process methods. Calls with a String argument will resolve to Parent.process(String) at compile time based on the static type, which can lead to unexpected behavior. Always use @Override to ensure you are actually overriding.
Another pitfall is forgetting that the return type must be covariant. If you try to return a completely unrelated type, the compiler will reject it. The rule is that the return type of the overriding method must be a subtype of the return type of the overridden method, or exactly the same.
Runtime Behavior and Method Dispatch
Overriding is central to dynamic method dispatch in Java. When you call a method on an object, the JVM selects the most specific implementation based on the runtime type of the object, not the compile-time type of the reference. This is what enables polymorphism.
Animal a = new Dog(); a.speak(); // calls Dog.speak() because the runtime type is Dog
This behavior is determined at runtime, and it is the reason the access and exception rules are enforced at compile time. The compiler ensures that the contract of the superclass method is preserved, so that any call valid on the superclass type remains valid on the subclass. This is also why you cannot reduce visibility or broaden checked exceptions: doing so would break the guarantee that a subclass can be used anywhere the superclass is expected.
Understanding these rules helps you design class hierarchies that are both safe and maintainable. When you override a method, you are promising that the new implementation respects the original contract, including its access level, exception behavior, and return type compatibility.