Code Companion
Java

Programming technique · B3.2.2 · Higher level only

Polymorphism

Inheritance gives related classes a shared role. Polymorphism becomes useful when the caller can use that shared role while each actual object supplies the behaviour that is right for its own type.

IB DP CS standard B3.2.2: Construct code to model polymorphism and its various forms, including dynamic behaviour such as method overriding and static polymorphic behaviour.
From T22: keep the inheritance idea, but change the question. Instead of asking “what does the child inherit?”, ask “when the same method call is made, which implementation actually runs?”

Why not ask the caller what type every object is?

Imagine a loop processing several related object types. A brittle design makes the loop inspect every object and decide what to do. A polymorphic design gives the related classes a common operation and lets each object provide the appropriate implementation.

Type-checking caller

The caller needs knowledge of Dog, Human, Bird and every future subtype. Adding a subtype means revisiting the dispatch logic and creates another place where a case can be forgotten.

Polymorphic caller

The caller asks every Animal for activity(). Each subtype owns its specialised implementation. New subtypes can join the same caller without rewriting the loop.

Polymorphism is not “different method names for different classes”. The useful pattern is one common operation, multiple valid implementations.

Dynamic polymorphism: override the inherited method

In Java, a child class can override an inherited instance method. @Override asks the compiler to verify that a matching parent method really exists. The important runtime idea is that an Animal reference may point to a Dog, Human or Animal object.

In Python, normal method lookup is dynamic. A subclass can define a method with the same name as the parent implementation. When the method is called, Python looks at the actual object and finds the appropriate implementation; there is no Java-style @Override annotation.

One caller, mixed objects

Every element in the array is used through the shared Animal role. The call expression stays exactly the same: member.activity(). Runtime dispatch selects the implementation from the actual object.

The list can contain related objects with the same expected operation. The loop does not need a declared parent reference type, but the call expression still stays exactly the same: member.activity().

ExpressionDeclared/reference typeActual object typeImplementation selected
group[0].activity()AnimalDogDog.activity()
group[1].activity()AnimalHumanHuman.activity()
group[2].activity()AnimalAnimalAnimal.activity()
ExpressionActual object typeImplementation selected
group[0].activity()DogDog.activity()
group[1].activity()HumanHuman.activity()
group[2].activity()AnimalAnimal.activity()

Overriding is not overloading

The guide also requires static polymorphic behaviour. A common Java example is method overloading: the class has the same method name with different parameter lists, and the matching signature is selected from the call and available types. This is a different mechanism from runtime overriding.

MechanismWhat changes?When is the choice made?Core example
Method overridingSubclass supplies its own inherited implementationRuntime, from the actual objectAnimal member = new Dog(...) then member.activity()
Java method overloadingSame class exposes the same method name with different parameter listsBefore runtime dispatch, from the applicable signaturelabel("Ari") versus label("Ari", 3)
Python boundary: default arguments can provide similar calling convenience, but they are still one method and should not be described as Java-style compile-time overloading. Do not invent false symmetry between the languages.
Efficiency boundary: overloading can make an API concise and reusable, but it does not automatically make the program run faster. The important syllabus distinction here is how the method form is selected.

Why polymorphism improves flexibility

Caller stays stable

A new subtype can provide the common operation without forcing every caller to add another type test.

Behaviour stays with the class

The code that makes a Dog behave like a Dog belongs in Dog rather than in a distant selection structure.

Common code is reusable

Collections, loops and methods can work with the shared parent role instead of duplicating logic for each subtype.

Still requires a sound hierarchy

Polymorphism does not rescue a poor inheritance design. The subtype must still make sense wherever the parent role is expected.

Trace the dispatch

Predict first. Reveal only after you can justify which method implementation is selected.

  1. Java: Animal member = new Dog("Mochi"); Which activity() implementation runs, and why?

    Reveal model answer

    Dog.activity() runs. The variable is declared as Animal, but the object referenced at runtime is a Dog, so dynamic dispatch selects the Dog override.

  2. If Human does not override activity(), what happens when a Human object receives activity()?

    Reveal model answer

    The inherited Animal.activity() implementation runs. Overriding is optional unless a later design, such as an abstract class, requires an implementation.

  3. A Bird subclass is added to the group. What should change in a well-designed polymorphic caller?

    Reveal model answer

    Add the Bird class, its activity() override and a Bird object. The loop that calls member.activity() should not need a Bird-specific type check.

  4. What is the key difference between Java method overloading and method overriding?

    Reveal model answer

    Overloading selects between different parameter signatures before runtime dispatch; overriding selects a subclass implementation at runtime based on the actual object. Python does not provide Java-style signature-based overloading.

What you should be able to construct

For this standard, recognition is not enough. You should be able to write a small parent–child hierarchy, override a shared operation, place different concrete objects behind a shared caller, and prove that one unchanged method call produces subtype-specific behaviour. In Java, you should also be able to apply a small overloaded-method example and distinguish it from overriding.

Challenges Choose one

Choose a challenge that feels appropriate for you. Code heat is only a rough estimate, not a fixed level.

Complete the Runtime Dispatch

Challenge ID: PC-T23-C01 · Standards: B3.2.2

Complete Dog and Human so that each overrides Animal.activity(). Then process Dog, Human and plain Animal objects through one Animal[] collection and one unchanged activity() call. Predict the three output lines before running the program. Do not use instanceof or type-checking selection to choose the behaviour.

Scaffold available

Add a New Form Without Changing the Loop

Challenge ID: PC-T23-C02 · Standards: B3.2.2

Extend the supplied hierarchy with Bird. Override activity(), add one Bird object to the existing Animal[] collection and prove that the existing loop works without modification. Add a short comment explaining how this demonstrates flexibility and reusability, and why adding a new subtype should not require another instanceof branch.

Scaffold available

Static or Dynamic?

Challenge ID: PC-T23-C03 · Standards: B3.2.2

Complete a Java ScoreLabel class with two overloaded label methods: one taking an int and one taking an int plus a String suffix. Run both calls, then comment on when Java chooses the overload and how this differs from runtime method overriding. Keep the example small: two signatures are enough.

Scaffold available
Next boundary: the parent class here is concrete and provides a default implementation. Requiring subclasses to implement a common operation through an abstract class belongs to B3.2.3 abstraction.