Java Polymorphism
›Short versionOpen if you only have a minute.
- A superclass or interface variable can hold a subclass instance. The compiler checks the variable's type.
- Overriding replaces an instance method body. The call uses dynamic dispatch: the runtime class picks the body.
- Overloading reuses a name with a different parameter list. The compiler picks the signature. That is static dispatch.
- Static methods, private methods, final methods, and fields do not override. The compile-time type decides which one you named.
- A subclass should still do what callers of the superclass were promised. Compiling is not the same as honoring the contract.
- @Override catches a signature typo. Without it, a near-miss becomes a new overload that never runs.
The checkout code asks a payment to capture forty rupees. Sometimes the object behind that variable is a card charge. Sometimes it is a bank transfer. The checkout code names neither class. is that arrangement: one call, and a body that follows the object.
The body follows the object
capture is an . The loop holds Payment references. reads the class of each object at the call.
abstract class Payment {
abstract void capture(int amount);
}
class CardPayment extends Payment {
@Override
void capture(int amount) {
System.out.println("card " + amount);
}
}
class BankPayment extends Payment {
@Override
void capture(int amount) {
System.out.println("bank " + amount);
}
}
Payment[] payments = { new CardPayment(), new BankPayment() };
for (Payment payment : payments) {
payment.capture(40);
}The first call prints card 40. The second prints bank 40. The loop did not branch on the class. Adding a third subclass means writing that subclass. The loop stays. That works only while the new class still means "capture this amount," which is .
The compiler chooses the signature
An is a second method, not a replacement. locks the signature in before the program runs.
void capture(int amount) {
System.out.println("int " + amount);
}
void capture(int amount, String memo) {
System.out.println("memo " + memo);
}
capture(40); // capture(int)
capture(40, "invoice"); // capture(int, String)If capture(int) is overridable, dynamic dispatch still picks the body of that one signature from the runtime class. It will not notice that a subclass also declared capture(long) and switch to it. The argument was an int, so the int signature was chosen at compile time. A subclass method with a different parameter list is an overload sitting next to the inherited method.
Static, private, and final methods skip the runtime lookup. So do fields. A subclass field named label hides the superclass field. Which one you read depends on the compile-time type of the reference, not on the object. Put the varying behavior in an instance method if you wanted the object to decide.
class Account {
static String kind() { return "account"; }
String label = "account";
}
class SavingsAccount extends Account {
static String kind() { return "savings"; }
String label = "savings";
}
Account a = new SavingsAccount();
a.kind(); // "account" — static dispatch on Account
a.label; // "account" — field hiding, compile-time typeWhat the caller was promised
Dynamic dispatch will cheerfully run a subclass that does not capture anything. The type system checked the signature, not the meaning. A subclass that returns success without moving money breaks every caller that trusted Payment. Refuse in the subclass only for reasons the superclass contract already allowed, or widen the superclass contract first so callers can hear the new failure.
$ javac Payment.java$ java Checkout
Account a = new SavingsAccount(); both types declare void describe(int) and static String kind(). What runs for a.describe(1) and a.kind()?
a.describe(1) runs SavingsAccount.describe if that method overrides the instance method. The runtime class supplies the body. a.kind() runs Account.kind because the method is static and the variable is declared as Account. The subclass method hides the static method. It does not override it.