Static vs Dynamic Binding — Practice Tasks¶
Twelve exercises in distinguishing dispatch types and reasoning about behavior.
Task 1 — Predict dispatch¶
class A { int x = 1; void m() { System.out.println("A.m, x=" + x); } }
class B extends A { int x = 2; @Override void m() { System.out.println("B.m, x=" + x); } }
A a = new B();
System.out.println(a.x); // ?
a.m(); // ?
((B) a).m(); // ?
B b = (B) a;
System.out.println(b.x); // ?
Predict each. Run and verify.
Task 2 — Static method "override"¶
class P { static String f() { return "P"; } }
class C extends P { static String f() { return "C"; } }
P p = new C();
System.out.println(p.f()); // ?
System.out.println(C.f()); // ?
System.out.println(((P) new C()).f()); // ?
Predict. Why does each produce its result?
Task 3 — Private method dispatch¶
class Parent {
private void compute() { System.out.println("Parent"); }
public void run() { compute(); }
}
class Child extends Parent {
private void compute() { System.out.println("Child"); }
}
new Child().run(); // ?
Predict. What's the rule about private methods and dispatch?
Task 4 — Constructor dispatch¶
class A {
A() { print("A.ctor"); print(); }
void print() { System.out.println("A.print"); }
}
class B extends A {
String name = "B";
@Override void print() { System.out.println("B.print, name=" + name); }
}
new B();
Predict the output. (Trick: A's ctor calls print() polymorphically; B.print runs but B.name isn't yet set.)
Task 5 — super is static¶
class A { void m() { System.out.println("A"); } }
class B extends A { @Override void m() { System.out.println("B"); super.m(); } }
class C extends B { @Override void m() { System.out.println("C"); super.m(); } }
new C().m();
Predict.
Task 6 — final method optimization¶
Write class Money { public final long cents() { return cents; } }. Use -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining to verify the JIT inlines cents() calls.
Task 7 — Megamorphic dispatch¶
Create an interface Op with int apply(int a, int b). Implement 5 ways. Loop calling apply on a list with mixed types. Profile with PrintInlining. Observe "callee not inlineable, megamorphic."
Task 8 — Sealed type dispatch¶
Define sealed interface Shape permits Circle, Square, Triangle { double area(); } with 3 record impls. Pattern-match in a switch. Compare with the megamorphic version from Task 7.
Task 9 — Static binding via final class¶
Create class Money { public Money plus(Money) { ... } } (non-final). Then final class Money2 { ... }. Benchmark plus calls in a loop with JMH. Compare.
Task 10 — Pattern matching vs polymorphism¶
Refactor:
double area(Shape s) {
if (s instanceof Circle c) return Math.PI * c.r() * c.r();
if (s instanceof Square sq) return sq.s() * sq.s();
return 0;
}
…to use polymorphism (Shape.area() abstract method). Then to pattern matching switch. Benchmark all three.
Task 11 — super.method chain¶
class A { void m() { System.out.print("A "); } }
class B extends A { @Override void m() { System.out.print("B "); super.m(); } }
class C extends B { @Override void m() { System.out.print("C "); super.m(); } }
class D extends C { @Override void m() { System.out.print("D "); super.m(); } }
new D().m();
Predict.
Task 12 — MethodHandle direct vs reflection¶
Compare: - Method.invoke(...) (reflection) - MethodHandle.invokeExact(...) (typed handle) - Direct method call
Benchmark with JMH for the same operation. Compare timing.
Validation¶
| Task | How |
|---|---|
| 1 | a.x=1 (field static), a.m()="B.m, x=2" (method dynamic; B.x), b.x=2 |
| 2 | "P", "C", "P" — static dispatch via declared type |
| 3 | "Parent" — Parent.run sees Parent.compute (private) |
| 4 | "A.ctor", "B.print, name=null" — overridable from ctor |
| 5 | C, B, A — super chains up |
| 6 | PrintInlining shows inline (hot) on cents() |
| 7 | PrintInlining shows "callee not inlineable, megamorphic" |
| 8 | Pattern match dispatch is faster (or comparable) and more readable |
| 9 | Final class likely faster (or no difference if JIT devirtualizes anyway) |
| 10 | Polymorphism wins on small monomorphic; pattern match wins on closed sets; instanceof chain is slowest |
| 11 | "D C B A " — chain runs top-down via super calls |
| 12 | Direct < MethodHandle < Reflection (orders of magnitude) |
Memorize this: dispatch is the language's mechanism for choosing which code runs. Static binding is fast, predictable, but rigid. Dynamic binding enables polymorphism. JIT collapses well-warmed dynamic dispatch to direct calls. Pattern matching is the modern alternative for closed hierarchies.