Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java polymorphism: overriding and dispatch

Last updated: 28 Sept 20263 min read
tutorial
IntermediateBy AITrove Editorial

Polymorphism lets a caller invoke an instance method through a common type while the runtime object selects the overridden implementation.

Dispatch follows the object

A billing caller needs a fee calculation, not knowledge of each payment channel’s implementation class. The PaymentFee interface establishes that contract. The object stored in its reference determines which instance implementation runs.

Overloading is different. When several methods have the same name but different parameter types, the compiler selects a signature from the declared types at the call site. Overriding chooses the implementation of that selected instance method at runtime.

Static methods do not participate in instance overriding. Fields are also resolved differently from virtual method calls. Designing an API around a shared method contract avoids expecting field access to behave like overridden behavior.

Preserve the caller’s assumptions

Both fee implementations accept nonnegative cents and reject invalid values. A substitute implementation should not silently accept a different unit or introduce surprising mutation into a calculation that callers regard as a query.

The cash calculation uses integer arithmetic with a minimum fee. The transfer calculation is fixed. These are teaching policies, not payment provider rates. Keeping policy values visible lets a test exercise the branch boundary.

Prefer an interface when implementations have unrelated storage or lifecycles. Use inheritance when the parent genuinely owns shared behavior and its invariants remain valid in every subtype. Interface contracts and inheritance boundaries address those decisions separately.

Working program

Java
public class PaymentFeeDispatch {
    interface PaymentFee {
        int feeCents(int amountCents);
    }
    static final class CashFee implements PaymentFee {
        public int feeCents(int amountCents) {
            if (amountCents < 0) throw new IllegalArgumentException("Negative amount");
            return Math.max(10, amountCents / 100);
        }
    }
    static final class TransferFee implements PaymentFee {
        public int feeCents(int amountCents) {
            if (amountCents < 0) throw new IllegalArgumentException("Negative amount");
            return 25;
        }
    }
    public static void main(String[] args) {
        PaymentFee[] policies = {new CashFee(), new TransferFee()};
        for (PaymentFee policy : policies) {
            System.out.println(policy.feeCents(1200));
        }
    }
}

Output

Output
12
25

Cost and design choices

The batch makes n calls for n policies, so the visible traversal is O(n), with O(1) extra loop state. The actual calculation cost belongs to each implementation. An interface does not promise a particular algorithmic complexity unless its contract states one.

Dispatch and JIT optimisation are implementation concerns. Do not assume every virtual call has a fixed expensive penalty, or that an interface abstraction requires creating another object for each call.

If a fee calculation later needs remote I/O, separate that work from a local arithmetic contract. A caller that expects a quick pure calculation should not unknowingly inherit retries and network timeouts.

Common Mistakes

  • Do not confuse overloading with overriding.
  • Use @Override when implementing an inherited method to catch signature mistakes.
  • Do not weaken validation or change units in a replacement implementation.

Connect the contracts

Compare the boundary explained in Function contracts with the assumptions made by this program.

java
polymorphism
Storage details