// project overview

Inheritance-Based vs Interface-Based Polymorphism (and Monomorphism)

This project asks one question in four languages: when a program calls speak(), how does the machine decide which code actually runs — and when does it stop having to decide at all?

Each language implementation (C, C++, java-implimentation, and rust-implimentation) walks through the same four scenarios using the same cast of characters (Dog, Cat, PureBreedDog). The scenarios are:

  1. Inheritance-Based Polymorphism — a base class declares an overridable method; subclasses override it; a call through a base-typed reference is resolved at runtime.
  2. Inheritance-Based Polymorphism w/ Defaults — a base class provides a concrete method that subclasses inherit unchanged, alongside an abstract method they must override. Demonstrates that inheritance carries both a dispatch contract and shared, non-overridden implementation.
  3. Interface-Based Polymorphism — a contract (interface / pure abstract class / trait) declares a method with zero shared implementation or state; multiple unrelated types implement it; a call through the interface-typed reference is resolved at runtime.
  4. Interface-Based Monomorphism — the compiler/runtime can prove there is only one possible target for a call (a final/sealed type, or a generic instantiated for one concrete type), so the indirection collapses to a direct call — or disappears into an inlined instruction stream entirely.

The throughline across all four languages: polymorphism is dispatch through an indirection (a pointer to a function or a table of them); monomorphism is the compiler/runtime proving that indirection is unnecessary and removing it. The four implementations differ mainly in where that indirection lives (a class vtable slot vs. an interface itable vs. a hand-rolled function pointer vs. a trait object's fat pointer) and who is able to eliminate it (a JIT devirtualizing a final call, or a compiler monomorphizing a generic before it ever emits code).

Why four languages

LanguageNative inheritance?Native interfaces?Dispatch mechanismSource
C No — simulated with structs No — simulated with structs Hand-rolled function-pointer table, called through a struct field C Implimentation.c
C++ Yes (class/virtual) Simulated via pure abstract classes Compiler-generated vtable, dispatched through a hidden vptr C++ Implimentation.cpp
Java Yes (extends) Yes (interface) JVM vtable (classes) vs. itable (interfaces), selected by distinct bytecode instructions java-implimentation/src
Rust No — no struct/class inheritance Yes (trait) Trait objects (dyn Trait, vtable via fat pointer) for polymorphism; generics for monomorphism rust-implimentation/src/main.rs

Java implementation files

Java is the only language here split across four files instead of one — there's no single "main" source file to link, so each part gets its own row:

PartFile
1 — Inheritance-Based Polymorphism Part1_InheritancePolymorphism.java
2 — Inheritance-Based Polymorphism w/ Defaults Part2_InheritanceWithDefaults.java
3 — Interface-Based Polymorphism Part3_InterfacePolymorphism.java
4 — Interface-Based Monomorphism Part4_InterfaceMonomorphism.java

The full java-implimentation/src directory (including the shared Animal/Dog/Cat types) is also browsable on GitHub.

C and Rust are the two edge cases, and both are informative for opposite reasons:

Part 1 — Inheritance-Based Polymorphism

A base type declares speak(); Dog and Cat override it; a function takes the base type and calls speak() without knowing the concrete type.

Bytecode/ASM evidence

Java's javap -c output for makeSpeak(Animal) shows the dispatch instruction directly:

public static void makeSpeak(Animal);
      Code:
      0: aload_0
      1: invokevirtual         #7    Method Animal.speak:()V
      4: return

invokevirtual is the JVM's vtable-based dispatch opcode — the concrete speak() invoked depends on the runtime type of the object on the stack, not the compile-time type of the reference. See the full listing in Part1_InheritancePolymorphism.java.

C's makeSpeak compiles to an explicit indirect call through a struct field — the same shape as invokevirtual, just without a JVM to hide it:

"makeSpeak":
      mov     rax, QWORD PTR [rbp-8]   ; rax = Animal*
      mov     rdx, QWORD PTR [rax]     ; rdx = a->speak  (load vtable-slot fn ptr)
      mov     rax, QWORD PTR [rbp-8]
      mov     rdi, rax                 ; arg0 = self
      call    rdx                      ; indirect call through the pointer

C Implimentation.asm

That call rdx is what invokevirtual and a C++ vptr call compile down to at the machine level: load a function pointer from memory, then call through the register. Nothing in Java or C++ is doing anything more fundamental than this — they just generate the pointer table and the load for you.

Part 2 — Inheritance-Based Polymorphism w/ Defaults

AbstractAnimal provides a concrete, non-overridden method (sleep()) alongside an abstract method (speak()) that DogWithDefault must implement. This isolates what inheritance adds beyond dispatch: shared implementation and shared state (age), which a bare interface cannot carry.

Bytecode evidence — the contrast with Part 1

public static void makeSleep(AbstractAnimal);
      Code:
      0: aload_0
      1: invokevirtual #7   // Method AbstractAnimal.sleep:()V
      4: return

Compare this to Part 1's invokevirtual Animal.speak. The bytecode instruction is identical in kind — the JVM does not know or care that sleep() happens to have only one implementation in this program. That knowledge is exactly what Part 4 (monomorphism) is about: a JIT that observes there is only one live implementation of a virtual method can devirtualize the call at runtime, replacing the vtable load with a direct call — something javap's static bytecode view cannot show, since it happens after JIT profiling. Full listing: Part2_InheritanceWithDefaults.java.

Part 3 — Interface-Based Polymorphism

ISpeaker/Speaker declares speak() with no shared state and no default implementation — a pure contract. InterfaceDog and InterfaceCat (unrelated types, not siblings in a class hierarchy) both implement it.

Part 4 — Interface-Based Monomorphism

The whole point of this section: dispatch collapses to a direct call once the compiler/runtime can prove only one implementation is reachable.

Cross-language summary

Inheritance polymorphismInheritance w/ defaultsInterface polymorphismMonomorphism
C Simulated: struct + fn-ptr field, indirect call rdx Shared fn called directly; only speak ptr is indirect Structurally identical simulation to inheritance case Plain struct, direct call by symbol — nothing to remove
C++ virtual, vtable, indirect call qword ptr [reg] Non-virtual method = direct call; virtual speak still indirect Pure abstract class → same vptr mechanism as inheritance final → compiler devirtualizes to direct call Type::method()
Java invokevirtual against class vtable invokevirtual, but only one implementation exists invokeinterface against itable — distinct opcode final → still invokevirtual in bytecode, but JIT devirtualizes/inlines at runtime
Rust N/A — no struct inheritance N/A &dyn Trait fat pointer → vtable call Generics → compiler monomorphizes, no vtable ever emitted

Key takeaways

  1. Polymorphism always costs an indirection — a function-pointer field, a vtable slot, an itable entry, or a fat pointer's vtable half. Every language here expresses that indirection differently, but it's present in every Part 1/2/3 case.
  2. Java is the only language that distinguishes inheritance-dispatch from interface-dispatch at the instruction level (invokevirtual vs. invokeinterface); C++ and hand-rolled C collapse both into the same mechanism, and Rust doesn't have inheritance-dispatch to compare against at all.
  3. Monomorphization is reachable by three different routes: C++'s final lets the compiler prove it ahead of time; Java's JIT proves it at runtime from observed call-site behavior (speculative devirtualization), and it can deoptimize if that assumption is later violated; Rust's generics never create the indirection to begin with, proving it at monomorphization time during compilation. C sidesteps the question entirely by never introducing a function pointer for PureBreedDog.
  4. "Interface" and "inheritance" are type-system concepts, not hardware ones. At the machine-code level, the C implementation shows that both collapse to "a struct holding a function pointer, called indirectly." The distinctions each higher-level language draws (separate opcodes, separate keywords, final/sealed annotations) exist to give the compiler and JIT more information about which indirections are safe to eliminate — that's the entire practical payoff of the distinction.

Where to look