JAVA / INHERITANCE AND POLYMORPHISM
Hiding versus overriding with static methods
Predict which body runs when a subclass redeclares a static method, and explain why the reference type, not the object, decides.
What you will learn
- Predict the result of a hidden static call made through a supertype reference
- Explain why static calls bind at compile time while instance calls bind at runtime
- Qualify static calls with a class name so the chosen method is visible in the source
- Spot the compile errors when a static and an instance method share one signature
Understanding Hiding versus overriding with static methods
Java lets a subclass declare a static method with exactly the signature of a static method it inherits, but that is not overriding. The language spec calls it hiding, and the difference is when the decision is made: an overridden instance method is chosen while the program runs, from the class of the actual object, whereas a static method is chosen while the file is compiled, from the type written in the source. Both versions keep existing, and the parent's version stays reachable by naming the parent class.
The reliable mental model is to ask what the compiler can see at the call site. VipTicket.type() names a class, so nothing is left to decide later. Ticket t = new VipTicket(); t.type() looks like a question aimed at an object, but the compiler uses only the declared type of t to pick the method and then discards the reference, so it emits an invokestatic instruction that names Ticket.type permanently; an instance call instead compiles to invokevirtual, which consults the method table of whatever object shows up.
The design consequence is that a static method can never be a polymorphic hook. Anything you expect a subclass to specialize must be an instance method, because hiding gives you two unrelated methods that happen to share a name. There is also no safety net: @Override is only valid for real overriding, so an accidental static clash compiles quietly and simply calls the wrong body. Hiding is fine when each class has its own factory or descriptor method and callers always write the class name.
class Ticket {
static String type() { return "Ticket"; }
String label() { return "generic label"; }
}
class VipTicket extends Ticket {
static String type() { return "VipTicket"; }
@Override
String label() { return "vip label"; }
}
public class Main {
public static void main(String[] args) {
Ticket t = new VipTicket();
VipTicket v = new VipTicket();
System.out.println("Ticket.type() = " + Ticket.type());
System.out.println("VipTicket.type() = " + VipTicket.type());
System.out.println("t.type() = " + t.type());
System.out.println("v.type() = " + v.type());
System.out.println("t.label() = " + t.label());
}
}A same-signature static method in a subclass hides rather than overrides, so the call binds to the compile-time type of its qualifier and never dispatches on the runtime object.
Worked examples
A static method calling a hidden static method
Shows that hiding cannot redirect a call that was already bound inside the parent class.
class Report {
static String header() { return "Report header"; }
static void print() { System.out.println("print() sees " + header()); }
}
class SalesReport extends Report {
static String header() { return "Sales header"; }
}
public class Main {
public static void main(String[] args) {
Report.print();
SalesReport.print();
System.out.println("direct call gives " + SalesReport.header());
}
}Example explained
Line 1header() inside print() has no qualifier, so the compiler resolves it against Report and freezes that choice.
Line 2SalesReport.print() runs the inherited Report.print() unchanged; SalesReport.header() never enters the picture.
Line 3Only the explicitly qualified SalesReport.header() reaches the hiding method, because the class name in the source decides.
Line 4If both header() methods were instance methods and print() were an instance method, the second line would read Sales header.
A null reference still resolves the static call
Demonstrates that a static call never touches the object, while an instance call must.
class Device {
static String kind() { return "Device"; }
String name() { return "a device"; }
}
class Printer extends Device {
static String kind() { return "Printer"; }
@Override
String name() { return "a printer"; }
}
public class Main {
public static void main(String[] args) {
Printer p = null;
System.out.println(p.kind());
try {
System.out.println(p.name());
} catch (NullPointerException e) {
System.out.println("name() threw NullPointerException");
}
}
}Example explained
Line 1p.kind() resolves to Printer.kind() from the declared type of p; the value of p is evaluated and then discarded.
Line 2Because no object is dereferenced, a null reference cannot fail here, which proves the object played no part in the choice.
Line 3p.name() must inspect the runtime object to select a body, so the null reference throws immediately.
Line 4Reading p.kind() as "ask this object which kind it is" is precisely the mental model that breaks.
Important notes
Hiding has its own rules: the hiding method must also be static, must not reduce visibility, and must have the same or a covariant return type; a static final method cannot be hidden at all.
A private static method is not inherited, so a same-signature static method in a subclass is a brand-new method rather than a hiding one, and the parent's version is simply unreachable from outside.
Common mistakes
Calling a static method through a variable, as in Ticket t = new VipTicket(); t.type(), and expecting the subclass value: the parent's method runs, the code compiles without complaint, and the wrong value flows downstream.
Putting @Override on the subclass static method to get a compiler check: compilation fails because hiding is not overriding, and removing the annotation removes the only hint that the two methods are unrelated.
Adding or dropping static on one side of a same-signature pair: javac rejects it with a cannot-override error mentioning that one method is static, and the fix is to make both static or both instance, not to rename one.
Try it yourself
Change, predict, then run
Add a static int priceInCents() to both Ticket and VipTicket returning different numbers, then print it through Ticket.priceInCents(), VipTicket.priceInCents(), and a Ticket-typed variable holding a VipTicket. Convert both methods to instance methods, rerun, and note which of the three lines changed.
Open the Java workspaceCheck your understanding
Given class A { static String who() { return "A"; } String tag() { return "a"; } } and class B extends A { static String who() { return "B"; } String tag() { return "b"; } }, what does A x = new B(); System.out.println(x.who() + x.tag()); print?
- Aa
- Ab
- Ba
- Bb
Show answer
who() is static, so the compiler resolves it from the declared type A and prints A regardless of the object; tag() is an instance method, so the runtime class B supplies the body and prints b. Bb is tempting if you assume every inherited member dispatches on the object, but only instance methods do.