Overriding is how a subclass says "for this method, I do it differently". It looks like a small thing, but the rules around it decide whether your code compiles, whether a base-class method can still be trusted by callers, and whether your hierarchy stays safe to change later.
What exactly counts as overriding?
The subclass declares a method with the same name and the same parameter types as an inherited method. That is the whole test. The parameters have to match exactly, because the compiler decides at compile time which method a call refers to, and it cannot guess that you meant to treat an int as a long.
The return type has more freedom: it may be the same as the parent's, or a narrower type that the parent could also return. Everything else about the method is inherited from the parent, including the documentation contract and any exception the caller is expected to handle.
public class Demo {
static class Employee {
private final String name;
Employee(String name) {
this.name = name;
}
String monthlyPay() {
return "flat 3000";
}
String describe() {
return name + " pays " + monthlyPay();
}
}
static class Manager extends Employee {
private final int teamSize;
Manager(String name, int teamSize) {
super(name);
this.teamSize = teamSize;
}
@Override
String monthlyPay() {
return "3200 plus a " + (teamSize * 150) + " team bonus";
}
@Override
String describe() {
return super.describe() + ", leading " + teamSize + " people";
}
}
public static void main(String[] args) {
Employee e = new Employee("Sam");
Manager m = new Manager("Nia", 4);
System.out.println(e.describe());
System.out.println(m.describe());
}
}Two lines of output, and the second one hides something worth pausing on. Manager.describe() calls super.describe(), which is the parent's version, and that parent version calls monthlyPay(). Because the object really is a manager, that call lands in Manager.monthlyPay() and not in the parent's flat rate. The manager's pay description comes out as 3200 plus a 600 team bonus. Which method runs is decided by the type of the object, not by the method you happen to be standing in.
Writing @Override above each one is optional but worth doing anyway: it tells the compiler to check your work, so a typo in the method name becomes a compile error instead of a silently new method that nobody calls.
What are the rules for the signature and the access level?
Three rules, and the compiler enforces all three.
- The name and the parameter types must match the inherited method exactly.
- The access level may stay the same or become wider, but never narrower.
- The return type may stay the same or become narrower, but never wider.
public class Demo {
static class Notification {
Object payload() {
return "generic";
}
void send() {
System.out.println("sent");
}
}
static class EmailNotification extends Notification {
@Override
public String payload() {
return "email body";
}
@Override
public void send() {
System.out.println("email sent");
}
}
public static void main(String[] args) {
EmailNotification e = new EmailNotification();
System.out.println(e.payload());
e.send();
System.out.println("length: " + e.payload().length());
}
}The parent returns Object and the child returns String, which is a narrower type and therefore allowed. The parent methods were package-private and the child versions are public, which is wider and also allowed. The output is email body, email sent and a length of 10. Returning a more specific type is genuinely useful: callers who hold an EmailNotification get a String without casting.
Which overriding mistakes does the compiler catch?
Two of the most common ones produce errors that look confusing until you know what they mean. The first is a parameter list that does not match:
public class Demo {
static class Base {
void run() {
}
}
static class Child extends Base {
@Override
void run(int times) {
}
}
}Demo.java:8: error: method does not override or implement a method from a supertype
@Override
^
1 errorrun(int) is a different method from run(), so the subclass has added a method rather than overridden one. The @Override annotation is what turns this into a clear error instead of a silent extra method.
The second is narrowing the access level, here from package-private to private:
public class Demo {
static class Base {
void run() {
}
}
static class Child extends Base {
@Override
private void run() {
}
}
}Demo.java:9: error: run() in Child cannot override run() in Base
private void run() {
^
attempting to assign weaker access privileges; was package
1 errorPrivate methods in a parent are not inherited at all, so a private run in the child is a completely separate method. That is why the compiler complains about access rather than about the override itself.
When should a subclass call super?
Call super.method() when the parent's version does something you still want, and you want to add to it rather than replace it. The common pattern is: do the shared part first through super, then add the specific part. If you do not need the parent behaviour at all, leave the call out and write the method from scratch.
There is one case where calling super is not optional. If the parent constructor has no no-argument version, the subclass constructor must call one of the parent's constructors explicitly, because the compiler inserts the no-argument call otherwise and the build fails.
What happens to static methods in a subclass?
They are hidden, not overridden. A static method belongs to the class, so a subclass method with the same name simply replaces the name in that class's namespace, and a call through the parent type still reaches the parent's version.
public class Demo {
static class Report {
static String kind() {
return "generic report";
}
String body() {
return "body";
}
}
static class Invoice extends Report {
static String kind() {
return "invoice";
}
@Override
String body() {
return "invoice body";
}
}
public static void main(String[] args) {
Invoice i = new Invoice();
System.out.println(i.body());
System.out.println(Invoice.kind());
System.out.println(Report.kind());
}
}The three lines make the distinction clear: the instance method is overridden, so i.body() prints invoice body, while the static method is hidden, so Invoice.kind() prints invoice and Report.kind() still prints generic report. The practical consequence is that a static method cannot be used polymorphically, which is why static helpers rarely belong in a class hierarchy at all.
Overriding rules at a glance
- Same name and same parameter types, or it is a different method.
- Access may widen, never narrow.
- Return type may narrow, never widen.
- Add
@Overrideso typos are caught at compile time. super.method()reuses the parent behaviour;this.method()calls your own.- Static methods are hidden rather than overridden.
- Private parent methods are not inherited and cannot be overridden.
Try it yourself: write a class Notification with a format() method returning the text "generic", and subclasses EmailNotification and SmsNotification that override it with "email: " and "sms: " prefixes. Now add a third level: PriorityEmailNotification extends EmailNotification that overrides format() to call super.format() and append " (priority)". Print the result of each. Then deliberately change one override to take a parameter and watch the compiler error, fix it, and finally try making the parent's method private to see what that does to the subclass versions.