Method Overriding and super in Java
A subclass replaces inherited behaviour by declaring the same signature, and the JVM chooses the version at runtime from the actual object.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
Definition
Overriding is a subclass providing its own implementation of a method already defined in its superclass, using the same name and parameter types. The decision about which version runs is made at runtime, based on the object rather than the declared type.
Example
class Notification {
void send(String recipient) {
System.out.println("Generic notification to " + recipient);
}
}
class EmailNotification extends Notification {
@Override
void send(String recipient) {
System.out.println("Email queued for " + recipient);
}
}
class SmsNotification extends Notification {
@Override
void send(String recipient) {
System.out.println("SMS queued for " + recipient);
}
}List<Notification> channels = List.of(
new EmailNotification(), new SmsNotification(), new Notification());
for (Notification channel : channels) {
channel.send("meera@example.com"); // a different method each time
}The loop variable is a Notification, yet the correct implementation runs for each object. This is dynamic dispatch, and it is the mechanism behind polymorphism.
The rules
| Element | Rule |
|---|---|
| Method name | Must be identical |
| Parameter list | Must be identical, in the same order |
| Return type | Same, or a subtype (covariant return) |
| Access | Same or wider, never narrower |
| Checked exceptions | Same, narrower, fewer, or none. Never broader |
| Unchecked exceptions | No restriction |
static methods | Hidden, not overridden |
private and final methods | Cannot be overridden |
Covariant return types
class Document {
Document copy() { return new Document(); }
}
class Invoice extends Document {
@Override
Invoice copy() { return new Invoice(); } // narrower return, allowed
}Exception rules in practice
class Reader {
void load() throws IOException { }
}
class CachedReader extends Reader {
@Override
void load() throws FileNotFoundException { } // narrower, allowed
// void load() throws Exception { } // broader, compile error
}The reason is substitutability: a caller holding a Reader reference has already written a handler for IOException, and must not be surprised by something wider.
The @Override annotation
class Base {
void process(String value) { }
}
class Derived extends Base {
@Override
void proccess(String value) { } // compile error: nothing is overridden
}Without @Override, a typo or a changed parameter type silently creates a new method instead of overriding, and the superclass version keeps running. Always write the annotation; it costs nothing and catches a class of bug that is otherwise very hard to see.The super keyword
class Payment {
double charge(double amount) {
return amount;
}
}
class CardPayment extends Payment {
@Override
double charge(double amount) {
double base = super.charge(amount); // extend rather than replace
return base + (base * 0.02); // add a two percent fee
}
}super has two uses: super.method() calls the superclass version, and super(...) calls a superclass constructor as the first statement of a constructor.
Overriding compared with overloading
| Aspect | Overriding | Overloading |
|---|---|---|
| Where | Across a class hierarchy | Within one class or inherited |
| Signature | Identical | Must differ |
| Bound | Runtime, from the object | Compile time, from declared types |
| Return type | Same or covariant | Unrestricted |
| Purpose | Specialised behaviour | Caller convenience |
Overriding Object methods
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Point p)) {
return false;
}
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point(" + x + ", " + y + ")";
}
}Common mistakes
- Changing a parameter type and accidentally overloading instead of overriding.
- Narrowing access, which will not compile.
- Declaring a broader checked exception in the override.
- Expecting a
staticmethod to be overridden; it is hidden and resolved from the declared type. - Overriding
equalswithouthashCode, which breaks every hash based collection. - Calling an overridable method from a constructor.
Best practices
- Always annotate with
@Override. - Preserve the contract of the method you are replacing; do not change what callers can rely on.
- Call
super.method()when the superclass behaviour should still happen. - Mark a method
finalwhen overriding it would break the class. - Keep overrides short; if one grows large, the design probably wants composition.
Practice
- Write three subclasses of a
Shapeclass that each overridearea(), and total the areas in a single loop. - Why does narrowing
publictoprotectedin an override fail to compile? - Predict the output when a subclass declares
void send(Object recipient)instead ofvoid send(String recipient). - Rewrite
CardPayment.chargewithoutsuperand explain what is lost. - Show what breaks when
equalsis overridden buthashCodeis not, using aHashSet.
Conclusion
Overriding replaces inherited behaviour and is resolved from the real object at runtime. Keep the signature exact, respect the contract, annotate every override, and use super when you mean to extend rather than replace.