8.2 Polymorphism and Dynamic Dispatch
Prerequisites: Inheritance, method signatures, and compile-time types Estimated time: 30 minutes You are ready to continue when: You can predict instance-method dispatch, identify members outside it, and keep downcasts at explicit boundaries
The previous page's EmailSender replaced the inherited send() implementation. That is method overriding; @Override asks the compiler to check that a method can actually be overridden. The next page covers signature and access rules. Here, we first examine which implementation a call selects.
The first four sections reuse the previous page's MessageSender and two-argument EmailSender, adding a demonstration SMS channel and a dispatch center. All channels only print messages:
class SmsSender extends MessageSender {
SmsSender(String message) { super(message); }
@Override public void send() {
logMessage();
System.out.println("demo SMS channel");
}
}
final class DispatchCenter {
void dispatch(MessageSender sender) {
java.util.Objects.requireNonNull(sender, "sender");
sender.send();
}
}1. Compile-Time Type Limits Calls; Runtime Type Selects Overrides
MessageSender email = new EmailSender("Capacity alert", "ops@example.com");
MessageSender sms = new SmsSender("Service recovered");
DispatchCenter center = new DispatchCenter();
center.dispatch(email);
center.dispatch(sms);The compile-time type of a variable is MessageSender, so the source code can only directly invoke members visible at that type. When executing sender.send(), the JVM selects between EmailSender.send and SmsSender.send based on the actual class of the receiver. This is known as dynamic dispatch of instance methods.
It is not the compiler generating a sequence of if (sender instanceof ...) at runtime. The JVM may employ strategies such as virtual method tables, inline caching, or devirtualization, but these do not alter the dispatch contract at the language level.
If the receiver is null, there is no actual class available for dispatch, and the call will throw NullPointerException.
2. The Value of Polymorphism Comes from Stable Contracts
When adding BeaconSender, the scheduler remains unmodified as long as the new type adheres to all the specifications of MessageSender.send. If a subclass arbitrarily throws exceptions on valid messages, alters the meaning of success, or disrupts observable ordering, "compilation success" does not imply safe replacement.
Therefore, extensibility is not an absolute guarantee that "adding any subclass with zero code changes" is possible. The caller remains closed only when it depends on a stable abstraction and the new implementation satisfies the contract. Configuration, dependency injection, and error handling may still require updates.
3. Static methods and fields do not participate in this dynamic dispatch
class Parent {
String field = "parent";
static String kind() { return "parent"; }
String instanceKind() { return "parent"; }
}
class Child extends Parent {
String field = "child";
static String kind() { return "child"; }
@Override String instanceKind() { return "child"; }
}
Parent value = new Child();
System.out.println(value.field); // parent
System.out.println(Parent.kind()); // parent
System.out.println(value.instanceKind()); // childOnly the last ordinary instance method undergoes override dispatch. Fields are resolved at compile time based on the expression's type, while static methods are bound to the class name.
4. Upcasting Safety, Downcasting Requires Proof
Assigning a subclass object to a parent-class variable is upcasting and does not require explicit casting:
MessageSender sender = new EmailSender("Message", "ops@example.com");The reverse, however, cannot be safely assumed based solely on variable type:
// EmailSender email = (EmailSender) sender;If the actual object is not of type EmailSender, a cast will throw ClassCastException. Java 17 supports pattern matching, which allows both validation and binding in a single expression:
if (sender instanceof EmailSender email) {
System.out.println(email.recipient());
}null instanceof EmailSender returns false. Pattern variables are only accessible within the scope where the compiler can prove the match has succeeded.
instanceof is suitable for parsing external boundaries, maintaining compatibility with legacy APIs, or when actual subclass-specific data is required. If a dispatch center routes based on concrete types and every new sender requires modifying the branching logic, it indicates a lack of appropriate abstraction or design patterns such as visitors.
5. Interfaces are usually better than concrete parent classes for callers
Using MessageSender makes class inheritance visible. A dispatch center that only needs a sending capability can depend on a smaller contract:
interface Sendable {
void send();
}
final class DispatchCenter {
void dispatch(Sendable sender) {
java.util.Objects.requireNonNull(sender, "sender").send();
}
}EmailChannel, SmsChannel, and ConsoleChannel can share an abstract base class or achieve common formatters through composition. As long as they implement Sendable, the scheduler doesn't need to care about their inheritance hierarchy. After the lesson on overriding, we will compare interfaces and abstract classes.
6. A Runnable Dispatch Example
Save this independent version as DispatchDemo.java, then compile and run it. Here, send() returns demonstration text. Keep this interface separate from the earlier version that returns void. It still performs no real delivery.
interface Sendable {
String send();
}
final class EmailChannel implements Sendable {
@Override
public String send() {
return "demo email channel";
}
}
final class SmsChannel implements Sendable {
@Override
public String send() {
return "demo SMS channel";
}
}
public class DispatchDemo {
static void dispatch(Sendable sender) {
System.out.println(sender.send());
}
public static void main(String[] args) {
dispatch(new EmailChannel());
dispatch(new SmsChannel());
}
}Output:
demo email channel
demo SMS channelProfessional Quick Reference
| Scenario | Choice |
|---|---|
| Only requires shared capabilities | Dependency on small interfaces |
| Needs shared stable state or skeleton | Controlled abstract base class |
| Only reuses implementation | Prefer composition or delegation |
| Must read subclass-specific information | Pattern matching at boundaries |
| Large number of concrete type branches | Re-examine the abstract contract |
Before Proceeding
Pass EmailChannel, SmsChannel, and ConsoleChannel to the same DispatchCenter. Mark which members are callable at compile time and which override is selected at runtime. Fields and static methods do not participate in this dispatch; an invalid downcast should produce ClassCastException, and null instanceof EmailSender should be false. If the core flow keeps branching on concrete types, revisit the abstraction contract.
→ Next: Method Overriding, @Override, and super
Specification reference: JLS §15.12.4.4: Runtime Selection of Target Method.