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 relies on it constantly: WPILib's MotorController type (in org.wpilib.hardware.motor) describes what every motor controller can do, and code written against it calls the same method on any motor controller without caring which class it actually is. (MotorController is an interface, a kind of type Ch.19 covers in full — for now, treat it like a superclass shared by every WPILib motor controller class.)
public void driveForward(MotorController controller)
{
// works the same whichever motor controller class is passed in
controller.setThrottle(0.5);
}
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:
RobotPart part = new Intake("Front Intake");
// declared type: RobotPart
// 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");
// COMPILE ERROR — Object has no indexOf method, even though String does
message.indexOf("h");
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):
RobotPart part = new Intake("Front Intake"); // declared RobotPart, actually an Intake
// the compiler only confirms RobotPart HAS a stop() method —
// but Intake's own override (Lesson 18.1) is what actually runs
part.stop();
This is exactly what makes the earlier RobotPart[]/ArrayList<RobotPart> examples (Lesson 17.3) genuinely useful — a single loop calling p.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 (RobotPart p : allParts)
{
// Intake's stop(), Climber's stop(), Shooter's stop() — whichever fits each object
p.stop();
}
Casting Back Down
Storing an Intake in a RobotPart variable is an upcast — from a subclass type up to a superclass type — and it happens automatically, because every Intake is a RobotPart. Going the other way, a downcast, needs an explicit cast, because not every RobotPart is an Intake. Suppose Intake adds a method of its own, a getter for the field from Lesson 17.2:
public class Intake extends RobotPart
{
// ...hasGamePiece field, constructor, and stop() as in Lessons 17.2 and 18.1
public boolean hasGamePiece() { return hasGamePiece; }
}
part.hasGamePiece() doesn't compile, since part is declared RobotPart. A cast tells the compiler to treat the reference as an Intake:
RobotPart part = new Intake("Front Intake"); // upcast: automatic
Intake intake = (Intake) part; // downcast: needs a cast
System.out.println(intake.hasGamePiece());
The cast is checked when the program runs. If the object isn't really an Intake, it throws a ClassCastException:
RobotPart other = new Climber("Left Climber");
// compiles, but throws ClassCastException when it runs: a Climber isn't an Intake
Intake wrong = (Intake) other;
So check with instanceof first. Since Java 16, the check and the cast combine into one step with a pattern: if part refers to an Intake, the new variable i is that same object, already typed as Intake, inside the if:
if (part instanceof Intake i)
{
System.out.println("Holding a piece: " + i.hasGamePiece());
}
Needing lots of downcasts is usually a sign the method belongs in the superclass as an override — polymorphism exists so most code never has to ask "which kind is this?"
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.
- Downcasting without checking.
(Intake) partcompiles wheneverpartcould be anIntake, but throwsClassCastExceptionat runtime if it isn't — guard it withinstanceof, or use thepart instanceof Intake ipattern. - 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
RobotPart-typed array or method parameter can transparently work withIntake,Climber,Shooter, or any other subclass — one piece of code, many possible real behaviors. - Upcasts are automatic; downcasts need an explicit cast, can throw
ClassCastException, and are safest asif (x instanceof Intake i). - 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") — the deck's vendor motor classes are replaced here by WPILib's generic MotorController interface