Template Method — Optimize¶
Each section presents a Template Method that works but is wasteful. Profile, optimize, measure.
Table of Contents¶
- Optimization 1: Mark template
finalfor JIT inlining - Optimization 2: Cache non-capturing lambdas
- Optimization 3: Async template with
thenCompose - Optimization 4: Avoid stateful base class
- Optimization 5: Replace inheritance with functional template
- Optimization 6: Sealed types for monomorphic dispatch
- Optimization 7: Inline hot path manually for tight loops
- Optimization 8: Precompute hooks at registration
- Optimization 9: Drop hooks that are always no-op
- Optimization 10: Move logic out of god-class base
- Optimization Tips
Optimization 1: Mark template final for JIT inlining¶
Before¶
public abstract class Pipeline {
public void run(Input input) {
validate(input);
process(input);
cleanup();
}
protected abstract void process(Input input);
}
run() is virtual. JIT must check at every call.
After¶
public abstract class Pipeline {
public final void run(Input input) {
validate(input);
process(input);
cleanup();
}
}
Measurement. JIT can devirtualize. Inline through run to its body. ~ns saved per call; matters in tight loops.
Lesson: Mark Template Methods final. Helps JIT and locks the algorithm.
Optimization 2: Cache non-capturing lambdas¶
Before¶
public List<User> findUsers() {
return jdbc.query("SELECT * FROM users",
rs -> new User(rs.getString("id"), rs.getString("name")) // lambda allocated per call
);
}
Each invocation allocates a new lambda.
After¶
private static final RowMapper<User> USER_MAPPER = rs ->
new User(rs.getString("id"), rs.getString("name"));
public List<User> findUsers() {
return jdbc.query("SELECT * FROM users", USER_MAPPER);
}
Measurement. Allocation rate drops; same instance reused across calls.
Trade-off. Less inline; slightly less readable.
Lesson: Static-final non-capturing lambdas avoid allocation in hot paths.
Optimization 3: Async template with thenCompose¶
Before¶
public Response process(Request req) {
Request validated = validate(req).join(); // blocks!
Request authed = authenticate(validated).join(); // blocks!
return handle(authed).join();
}
Each .join() blocks the calling thread.
After¶
public CompletableFuture<Response> process(Request req) {
return validate(req)
.thenCompose(this::authenticate)
.thenCompose(this::handle);
}
Measurement. Throughput rises with available threads. No blocking on async results.
Lesson: Async templates compose futures; never .join() mid-template.
Optimization 4: Avoid stateful base class¶
Before¶
public abstract class Pipeline {
private List<String> buffer = new ArrayList<>(); // shared mutable state
public final void run(String input) {
buffer.clear(); // reset
process(input);
save(buffer);
}
protected final void emit(String item) { buffer.add(item); }
protected abstract void process(String input);
}
Concurrent calls race on buffer.
After¶
public abstract class Pipeline {
public final void run(String input) {
List<String> buffer = new ArrayList<>();
process(input, buffer);
save(buffer);
}
protected abstract void process(String input, List<String> buffer);
}
Measurement. Concurrent-safe. No synchronization needed.
Lesson: Stateless base classes are concurrency-friendly. Pass state through method parameters.
Optimization 5: Replace inheritance with functional template¶
Before¶
public abstract class Importer {
public final void run(String path) {
var data = read(path);
var rows = parse(data);
save(rows);
}
protected abstract List<Row> parse(String data);
}
class CsvImporter extends Importer { /* ... */ }
class JsonImporter extends Importer { /* ... */ }
class XmlImporter extends Importer { /* ... */ }
Inheritance hierarchy; one class per format.
After¶
public final class Importer {
public static void run(String path, Function<String, List<Row>> parser) {
var data = read(path);
var rows = parser.apply(data);
save(rows);
}
}
// Usage:
Importer.run("data.csv", CsvParser::parse);
Importer.run("data.json", JsonParser::parse);
Measurement. Less code, less hierarchy. Easier to test.
Trade-off. Less type structure; less self-documentation.
Lesson: Functional Template Method via callbacks is lighter and more flexible than inheritance.
Optimization 6: Sealed types for monomorphic dispatch¶
Before¶
public abstract class Beverage {
public final void make() { /* ... */ }
protected abstract void brew();
}
class Tea extends Beverage { ... }
class Coffee extends Beverage { ... }
class Mocha extends Coffee { ... }
class Latte extends Coffee { ... }
// ... unbounded subclass set; megamorphic call site
JIT can't predict subclass types.
After (Java 17+)¶
public sealed abstract class Beverage permits Tea, Coffee {}
public sealed abstract class Coffee extends Beverage permits Mocha, Latte {}
Bounded set; pattern matching exhaustive; JIT specializes.
Measurement. Slight JIT improvement; refactoring safer.
Lesson: Sealed hierarchies enable JIT optimization and compile-time exhaustiveness.
Optimization 7: Inline hot path manually for tight loops¶
Before¶
abstract class Updater {
public final void tick(List<Entity> entities) {
for (Entity e : entities) update(e); // megamorphic if many Updater types
}
protected abstract void update(Entity e);
}
For 10K entities × 10 updater types: vtable cost ~30µs.
After (manual specialization)¶
public final class TightLoop {
public static void tick(EntityList list) {
switch (list.type) {
case PHYSICS -> tickPhysics(list);
case AI -> tickAi(list);
case RENDER -> tickRender(list);
}
}
private static void tickPhysics(EntityList list) {
for (PhysicsEntity e : list.physicsEntities) {
// fully monomorphic; JIT inlines
e.position.add(e.velocity);
}
}
}
Measurement. Per-tick savings of µs; matters in 60+fps loops.
Trade-off. More code; less polymorphic flexibility. Use only in measured hot paths.
Lesson: For ultra-hot inner loops, hand-specialize beyond polymorphic dispatch.
Optimization 8: Precompute hooks at registration¶
Before¶
Polymorphic dispatch per call per plugin.
After¶
private final List<Runnable> beforeHooks = new ArrayList<>();
private final List<Runnable> handleHooks = new ArrayList<>();
private final List<Runnable> afterHooks = new ArrayList<>();
public void register(Plugin p) {
beforeHooks.add(p::before);
handleHooks.add(p::handle);
afterHooks.add(p::after);
}
public final void run() {
for (Runnable r : beforeHooks) r.run();
for (Runnable r : handleHooks) r.run();
for (Runnable r : afterHooks) r.run();
}
Measurement. Tighter loops; better cache locality. Only registered hooks actually present.
Lesson: Convert plugin polymorphism to flat lists of method references; JIT optimizes better.
Optimization 9: Drop hooks that are always no-op¶
Before¶
public abstract class Workflow {
public final void run() {
beforeStart();
process();
afterStart();
beforeProcess();
afterProcess();
beforeEnd();
afterEnd();
}
protected void beforeStart() {}
protected void afterStart() {}
protected void beforeProcess() {}
protected void afterProcess() {}
protected void beforeEnd() {}
protected void afterEnd() {}
}
7 hooks; 5 always no-op in practice.
After¶
public abstract class Workflow {
public final void run() {
process();
}
protected abstract void process();
}
Measurement. Less code; less indirection; faster.
Lesson: Audit hooks. Drop those nobody uses. If everyone overrides one to inject the same thing, make it part of the template.
Optimization 10: Move logic out of god-class base¶
Before¶
public abstract class GodBase {
public final void run() {
// 20 hooks
beforeXxx(); xxx();
beforeYyy(); yyy();
beforeZzz(); zzz();
// ...
}
// 20 abstract methods + 20 hooks
}
Subclasses must implement 20 things; most are stubs.
After¶
public final class CompositeWorkflow {
private final List<Step> steps;
public CompositeWorkflow(List<Step> steps) { this.steps = steps; }
public void run() { for (Step s : steps) s.run(); }
}
interface Step { void run(); }
Measurement. Each Step is simple, focused, testable. No god-class; composition over inheritance.
Lesson: When Template Method becomes a god class, refactor to composition (Strategy / middleware / pipeline).
Optimization Tips¶
- Mark template
final. JIT inlining + algorithmic integrity. - Cache non-capturing lambdas as static fields. No allocation.
- Async templates compose futures. Never
.join()mid-template. - Stateless base class for concurrency. Pass state through parameters.
- Functional Template Method for flexibility. Callbacks over inheritance.
- Sealed hierarchies for bounded subclass sets. JIT specialization.
- Manual specialization for tight loops. Last resort; profile first.
- Precompute plugin hooks as flat lists.
- Drop unused hooks. Audit periodically.
- Refactor god-class templates to composition.
- Profile before optimizing. Template Method dispatch is rarely the bottleneck.