Polymorphism
Many Forms, One Piece of Code
Polymorphism ("many forms") means the method that actually runs at runtime depends on the object's actual type — not the type its variable was declared with. This is the concrete payoff of everything Ch.17 and Lessons 18.1-18.2 built toward: real WPILib code treats a TalonFX or a SparkMax as just a MotorController, and calls the same method on either without caring which one it actually is:
public void driveForward(MotorController controller)
{
controller.set(0.5); // works identically whether controller is a TalonFX or a SparkMax
}
Declared Type vs. Actual Type
Every variable has two types that matter: its declared (compile-time) type — what it's written as in the code — and its actual (runtime) type — the real class that was actually constructed with new:
Subsystem s = new Intake("Front Intake");
// declared type: Subsystem
// actual type: Intake
The compiler only ever checks the declared type when deciding whether code compiles — it verifies that whatever method you're calling actually exists on that declared type (or an ancestor of it):
Object message = new String("hi");
message.indexOf("h"); // COMPILE ERROR — Object has no indexOf method, even though String does
Even though the real object here is a String (which absolutely does have indexOf), the compiler only looks at message's declared type, Object — and refuses to compile, since Object never promises an indexOf method.
Runtime Dispatch: The Actual Type Decides Which Version Runs
Once code does compile, the JVM decides which overridden version of a method actually executes by looking at the object's real, actual type — starting there and searching upward through its superclasses if needed (same lookup process as Lesson 18.2's super):
Subsystem s = new Intake("Front Intake"); // declared Subsystem, actually an Intake
s.stop(); // the compiler only confirms Subsystem HAS a stop() method — but Intake's own override is what actually runs
This is exactly what makes the earlier Subsystem[]/ArrayList<Subsystem> examples (Lesson 17.3) genuinely useful — a single loop calling s.stop() on each element automatically runs whichever subclass's own stop() actually applies, without an if/else chain checking each one's specific type:
for (Subsystem s : allSubsystems)
{
s.stop(); // Intake's stop(), Climber's stop(), Shooter's stop() — whichever is actually correct for each object
}
Static Methods Don't Get This
Polymorphic dispatch only applies to regular (instance) methods. A static method call is resolved entirely by the declared type, at compile time — there's no runtime lookup involved at all, since static methods belong to the class itself, not to any particular object.
Common Pitfalls
- Assuming the compiler checks the actual type. It never does —
message.indexOf("h")fails to compile even when the real object genuinely has that method, because the compiler only ever consults the declared type. - Forgetting that runtime dispatch is what actually decides which override runs. The declared type only gates whether the code compiles — which specific version executes is always decided by the real, actual object type.
- Expecting polymorphism to apply to static methods. Static method calls are resolved by declared type alone, with no runtime dispatch — they don't participate in overriding the way instance methods do.
Key Takeaways
- Polymorphism means the method that runs depends on an object's actual runtime type, not its variable's declared type.
- The compiler only checks the declared type for whether a method call is legal; the JVM decides which overridden version actually runs based on the real, actual type.
- This is why a
Subsystem-typed array or method parameter can transparently work withIntake,Climber,Shooter, or any other subclass — one piece of code, many possible real behaviors. - Static methods don't participate in this — they're resolved entirely by declared type, at compile time.
Derived from csawesome-2026: unit-5-inheritance-optional/5.06-polymorphism.md (CSA 5.6)
Deck context: mechacoder-test/src/lessons/java-2.js, slide 5 ("Polymorphism: Many Forms") — TalonFX/SparkMax-as-MotorController example