Overloading and overriding: two decisions in one call
Choose a method signature from static argument types, then follow the selected instance method to the runtime override. Inheritance and polymorphism · Object-oriented programming.
When you call a method in Java, two separate choices happen: which version of the method fits the arguments you wrote, and which class's code then runs for the object you called it on. Keep those two choices apart before you trace the result.
Split one call into two decisions
Suppose ref is declared as Formatter, but the object created for it is a FancyFormatter. The call ref.show("hi") carries two kinds of information: the type written in the code, which the compiler can see (the compile-time type), and the object that actually exists while the program runs (the runtime object). Java uses them at different moments.
Java: choose the signature, then the implementation
Read the steps as text
- Formatter declares show(Object) and show(String). FancyFormatter extends Formatter and declares show(String). No call has run yet.
- The call ref.show("hi") starts. The reference ref is typed Formatter and holds a FancyFormatter object. The argument "hi" has type String. Java chooses the signature first, using only the types written in the code.
- The signature show(Object) accepts an argument typed String, so it applies.
- The signature show(String) accepts an argument typed String, so it applies.
- Both show(Object) and show(String) apply; show(String) is the more specific, so Java selects it. The signature is now fixed, and the object's class has not been searched yet.
- Now the search for exactly show(String) starts at the object's own class, FancyFormatter. FancyFormatter declares it, so the search stops here.
- The implementation FancyFormatter.show(String) runs and returns "fancy-string".
First, Java searches the overloads visible from the compile-time type and checks the argument expressions’ compile-time types. The argument is a String, so show(String) is more specific than show(Object). This is overloading: same name, different formal parameter types. A return type alone cannot create another overload.
Only after that signature is selected does ordinary instance dispatch matter. The selected signature is still show(String); the runtime object can supply FancyFormatter.show(String). This is overriding: a subclass provides a new implementation for the inherited instance signature.
The two decisions can agree on the same method name while using different evidence. Keep a small note beside the call: static types choose the signature, and the runtime object chooses the implementation only for that signature.
Transfer this call to a variable whose declared argument type is Object, even if that variable currently holds a String. Java now considers the Object expression when choosing among overloads, so show(Object) is selected here; the value’s runtime class does not retroactively change that signature choice. The receiver is a separate question: ref may be declared Formatter while its runtime object is FancyFormatter, allowing the already-selected instance signature to reach the child. Because FancyFormatter does not override show(Object), the inherited Formatter version runs, even though the receiver is a FancyFormatter. Keeping argument and receiver roles separate prevents a common “runtime picks everything” shortcut.
Java: the declared type of the argument decides
Read the steps as text
- Formatter declares show(Object) and show(String). FancyFormatter extends Formatter and declares show(String). No call has run yet.
- The call ref.show(text) starts. The reference ref is typed Formatter and holds a FancyFormatter object. The variable text is declared as Object and holds "hi". Java chooses the signature first, using only the types written in the code.
- The signature show(Object) accepts an argument typed Object, so it applies. Only the declared type counts, not the value it holds.
- The signature show(String) needs a String, but the argument is typed Object, so it does not apply. Only the declared type counts, not the value it holds.
- Only show(Object) applies, so Java selects it. The signature is now fixed, and the object's class has not been searched yet.
- Now the search for exactly show(Object) starts at the object's own class, FancyFormatter. FancyFormatter does not declare it.
- The search continues in Formatter, still looking only for show(Object). Formatter declares it, so the search stops here.
- The implementation Formatter.show(Object) runs and returns "object".
In real code, overload when the same idea takes different input types, such as print(int) and print(String). Prefer distinct names such as parseInt and parseDouble when the meaning differs, because a reader cannot tell the chosen version from the call. Override when a subclass must change behaviour behind a shared type, such as a PaymentMethod reference calling a card or UPI version.
See the paired implementations
These examples return small labels so the two decisions stay visible. Java has an overload set in Formatter; the child overrides only the String signature. Python has overriding, but its class body binds one show name at a time, so there is no Java-style overload set from these definitions.
class Formatter {
String show(Object value) { return "object"; }
String show(String value) { return "string"; }
}
class FancyFormatter extends Formatter {
@Override String show(String value) { return "fancy-string"; }
}
class Main {
public static void main(String[] args) {
Formatter ref = new FancyFormatter();
System.out.println(ref.show("hi")); // fancy-string
}
}class Formatter:
def show(self, value):
return "object-like"
class FancyFormatter(Formatter):
def show(self, value):
return "fancy"
print(FancyFormatter().show("hi")) # fancyThe outputs are not meant to match character for character. The structural pairing is the point: Java chooses show(String) before the child implementation runs; Python finds the child’s show through runtime attribute lookup. Python’s super() or an explicit Formatter.show(self, ...) is a deliberate base call, not overload selection.
Keep the exceptions small
An ordinary Java instance method is the core case. The word “override” becomes misleading when the declaration is static or private, and a field is selected by the reference expression rather than dynamically overridden. Treat those as boundary checks, not as more versions of the same mechanism.
A matching static method in a subclass hides the superclass static method; it does not override it. A private superclass method is not inherited for overriding, so a same-signature child method is separate and must not carry @Override. Fields can be hidden by a child field, but ref.field is selected from the compile-time type of ref. These cases do not use the ordinary virtual instance path from the first scene.
Java: static methods and fields are bound by the declared type
Read the steps as text
- Formatter declares tag() and mode. FancyFormatter extends Formatter and declares tag() and mode. No call has run yet.
- The call ref.tag() starts. The reference ref is typed Formatter and holds a FancyFormatter object. The call passes no argument. Java chooses the signature first, using only the types written in the code.
- The signature tag() takes no argument and the call passes none, so it applies. Only tag() applies, so Java selects it. The signature is now fixed, and a static method is bound at compile time, so the object's class will not be searched.
- The method tag() is static, so it is bound at compile time to Formatter, found through the declared type. FancyFormatter declares its own tag(), which only hides it, and the object's class is not searched.
- The static method Formatter.tag() runs and returns "base-tag". The object's own class had no say.
- Reading ref.mode starts. The reference ref is typed Formatter and holds a FancyFormatter object. Java first finds the field from the declared type.
- The field name mode is resolved from the declared type Formatter, not from the object's class.
- The field mode is read from Formatter, the row found through the declared type. The object's class FancyFormatter is not searched, even if it declares its own.
- The read returns "base" from the Formatter row.
Python’s repeated-name trap is different. In one class body, a later def add binds add again. It replaces the earlier function instead of producing add(a, b) and add(a, b, c) overloads. A two-argument call therefore raises TypeError if the surviving definition requires three arguments. Defaults, *args, or explicit dispatch are implementation techniques, not a Java overload set.
class Calc:
def add(self, a, b):
return a + b
def add(self, a, b, c): # rebinds the name add
return a + b + c
Calc().add(1, 2) # TypeError: missing 1 required positional argument: 'c'Python: a later def replaces the earlier one
Read the steps as text
- The class Calc is empty. Its body binds names one definition at a time.
- The class body runs def add(self, a, b). The class Calc now binds the name add to this function.
- The call Calc().add(1, 2) follows the one live add, which fits 2 arguments, so it returns normally.
- The class body runs def add(self, a, b, c). It binds the name add again, so the earlier function is replaced and only one add stays live.
- The call Calc().add(1, 2) follows the one live add, which cannot take 2 arguments, so Python raises TypeError: missing c.
- The class body runs def add(self, a, b, c=0). It binds the name add again, so the earlier function is replaced and only one add stays live. The parameter c now has a default, so a call may leave it out.
- The call Calc().add(1, 2) follows the one live add. Its default fills in c, so the call returns normally.
Recap
- Overloading changes the available signatures under one name; Java selects one using compile-time information.
- Overriding changes the implementation of an inherited instance signature; runtime dispatch can reach the child implementation.
- Static/private members and fields follow different rules, and repeated Python definitions replace a name.
Go deeper
The later dispatch lesson owns a fuller lookup trace and implementation notes. This lesson stops at the handoff between signature selection and instance dispatch so the two decisions stay separable. Java also has additional restrictions, such as final methods and access checks, that are outside this compact contrast.
If two unrelated reference overloads are both applicable, Java may report an ambiguity instead of selecting one. A null example therefore needs its own explanation of specificity and ambiguity; it is omitted here so the main call stays predictable.
Check your understanding, then continue to abstract classes and interfaces; the later method dispatch lesson traces method lookup in more detail. Practise dynamic behavior in Linked List Fundamentals, where the operation you call and the node reached are separate parts of the trace.