Dispensables — Optimize¶
12 inefficient cures for Dispensables.
Optimize 1 — Over-extracted method (Java)¶
Original:
public boolean isAdult(int age) {
return checkAge(age);
}
private boolean checkAge(int age) {
return verifyNonNegative(age) && verifyOver18(age);
}
private boolean verifyNonNegative(int age) {
return checkNotNegative(age);
}
private boolean checkNotNegative(int age) {
return age >= 0;
}
private boolean verifyOver18(int age) {
return age >= 18;
}
Issue: Extract Method applied too aggressively. 5 method calls to evaluate age >= 18. Each is a JIT inline candidate, but accumulates IC pressure.
Fix: consolidate.
The original had no validation step that mattered (negative ages). If you need it, if (age < 0) throw ... once at the boundary, not as nested helpers.
Optimize 2 — Premature DRY (Python)¶
Original:
# Two callers needed similar but slightly different formatting:
def format_address(addr, include_country=True, abbreviate_state=False,
add_zip_extension=False, format='multiline', uppercase=False):
parts = []
if uppercase:
addr = {k: v.upper() for k, v in addr.items()}
parts.append(addr['line1'])
if abbreviate_state:
state = STATE_ABBREVIATIONS[addr['state']]
else:
state = addr['state']
# ... 30 more lines handling all combinations
Issue: parametrized for two callers; now serves 8 with a 6-arg call signature. Each new caller adds a parameter; eventually unmaintainable.
Fix: stop unifying. Have two (or more) focused functions per caller:
def format_us_address(addr): ...
def format_intl_address(addr): ...
def format_label_address(addr): ...
Some duplication. Each is simple. Sandi Metz: "Duplication is far cheaper than the wrong abstraction."
Optimize 3 — Unnecessary indirection through Lazy Class (Go)¶
Original:
type StringProcessor struct {
upper bool
}
func NewStringProcessor() *StringProcessor {
return &StringProcessor{upper: false}
}
func (s *StringProcessor) Process(input string) string {
if s.upper {
return strings.ToUpper(input)
}
return input
}
// Hot path:
processor := NewStringProcessor()
for _, line := range millions {
output := processor.Process(line)
}
Issue: processor.Process is a virtual call (interface, in Go terms). One field lookup, one branch — every call.
Fix: when upper is always false (common case), eliminate the processor:
Or, when upper matters, partial application at construction:
func MakeProcessor(upper bool) func(string) string {
if upper {
return strings.ToUpper
}
return func(s string) string { return s }
}
processor := MakeProcessor(false) // returns identity
for _, line := range millions {
output := processor(line)
}
The function pointer is direct; no struct field access per call.
Optimize 4 — String concatenation in Data Class (Java)¶
Original:
class Customer {
private String firstName, lastName, country;
public String fullName() {
return firstName + " " + lastName + " (" + country + ")";
}
}
// Hot path: list of 1M customers, format each
List<String> formatted = customers.stream().map(Customer::fullName).toList();
Issue: every fullName() allocates a StringBuilder (implicitly via +), then a String. 1M+ allocations.
Fix 1: if hot enough, pre-allocate.
public String fullName(StringBuilder buf) {
buf.setLength(0);
return buf.append(firstName).append(' ')
.append(lastName).append(" (")
.append(country).append(')')
.toString();
}
// Reuse buf in hot loop:
StringBuilder buf = new StringBuilder(64);
List<String> formatted = customers.stream().map(c -> c.fullName(buf)).toList();
Fix 2: if values are immutable, cache:
class Customer {
private final String firstName, lastName, country;
private String fullNameCache; // computed lazily
public String fullName() {
if (fullNameCache == null) {
fullNameCache = firstName + " " + lastName + " (" + country + ")";
}
return fullNameCache;
}
}
Trade-off: caching adds field; only worth it if fullName() is called many times per Customer.
Optimize 5 — Speculative interface forces megamorphism (Java)¶
Original:
interface Validator { boolean validate(Object o); }
class EmailValidator implements Validator { ... }
class PhoneValidator implements Validator { ... }
class AddressValidator implements Validator { ... }
class CreditCardValidator implements Validator { ... }
class IbanValidator implements Validator { ... }
class SsnValidator implements Validator { ... }
// Hot path:
for (Field f : fields) {
Validator v = registry.get(f.type);
boolean ok = v.validate(f.value); // megamorphic — 6 types
}
Issue: 6 types at one call site → megamorphic, vtable cost.
Fix: if validators are stateless, group by type at the caller:
Map<FieldType, List<Field>> byType = fields.stream().collect(groupingBy(f -> f.type));
for (var entry : byType.entrySet()) {
Validator v = registry.get(entry.getKey());
for (Field f : entry.getValue()) {
v.validate(f.value); // monomorphic per group
}
}
Throughput improves; same correctness.
Optimize 6 — Encapsulated collection causes copying (Java)¶
Original:
class Order {
private final List<LineItem> items;
public List<LineItem> getItems() {
return new ArrayList<>(items); // defensive copy
}
}
// Hot path:
for (Order o : orders) {
for (LineItem item : o.getItems()) { // copies list every call
...
}
}
Issue: defensive copy per call. With 1M orders × 50 items, 1M list allocations.
Fix: unmodifiable view (no copy).
Or expose an iterator:
Or a forEach:
The forEach version is "Tell, Don't Ask" — caller can't mutate; no copy needed.
Optimize 7 — Form Template Method with hooks adds dispatch (Java)¶
Original:
abstract class Workflow {
public final void run() {
beforeStep1();
step1();
afterStep1();
beforeStep2();
step2();
afterStep2();
}
protected void beforeStep1() {} // empty default
protected void afterStep1() {}
protected void beforeStep2() {}
protected void afterStep2() {}
protected abstract void step1();
protected abstract void step2();
}
// 1 subclass that overrides nothing:
class SimpleWorkflow extends Workflow {
protected void step1() { ... }
protected void step2() { ... }
}
Issue: 4 empty hook calls per run(). JIT can inline empties (zero cost), but the method body is bloated.
Fix: if no subclass uses the hooks, remove them.
abstract class Workflow {
public final void run() {
step1();
step2();
}
protected abstract void step1();
protected abstract void step2();
}
If hooks are needed in 1-2 subclasses but not most, accept the empty hooks (JIT will inline). Don't remove; some flexibility is worth the cost.
Optimize 8 — Defensive copying in immutable types (Java)¶
Original:
final class Person {
private final List<String> aliases;
public Person(List<String> aliases) {
this.aliases = new ArrayList<>(aliases); // defensive copy on construction
}
public List<String> getAliases() {
return new ArrayList<>(aliases); // defensive copy on access
}
}
Issue: two copies per use — once on construct, once on access.
Fix: use immutable collections.
final class Person {
private final List<String> aliases;
public Person(List<String> aliases) {
this.aliases = List.copyOf(aliases); // immutable
}
public List<String> getAliases() {
return aliases; // already immutable
}
}
List.copyOf returns an immutable list — safe to share. One copy on construct, zero on access.
Optimize 9 — Dead code in libraries (Java)¶
Issue: a library has dead code. Library users still ship it (it's in the JAR).
Fix: for size-sensitive consumers (Android apps, serverless functions), use:
- ProGuard / R8 to remove unreferenced code at build time.
- JLink / GraalVM native-image to produce a custom JRE/binary with only used classes.
- Tree-shaking for JS modules.
This is build-time DCE, distinct from runtime JIT DCE.
Optimize 10 — Comments compiled into bytecode? (Java)¶
Reality: comments are stripped by the compiler — they don't appear in .class files. Even Javadoc isn't in bytecode (it's in separate .html files).
However: annotation strings are in bytecode. @Deprecated(since = "very long string") consumes class file space.
Fix: keep annotation values short. For long descriptions, use Javadoc, not annotation strings.
Optimize 11 — Dead public methods in API surface (Java)¶
Original: a library has chargeViaLegacyGateway() marked @Deprecated. It's still public. Subclasses might override.
Issue: @Deprecated doesn't prevent use. Static analysis flags new uses, but existing uses (and subclass overrides) keep the method effectively alive.
Fix:
- Mark with
@Deprecated(forRemoval = true, since = "2.5")— clearer intent, future tooling support. - JEP 277 (final removal in major version): in version 3.0, delete.
- Communicate via deprecation notes: docs, release notes, migration guide.
In Java 9+, with module-info.java, you can also remove the export — making the method invisible to consumers.
Optimize 12 — Inline Class with side effects (Java)¶
Original (Lazy Class):
class Logger {
public void log(String msg) {
System.out.println("[" + Instant.now() + "] " + msg);
}
}
// Used:
new Logger().log("Hello"); // allocates Logger per call
Naive Inline:
Issue: the inlining lost the consistency of timestamp formatting. If formatting needs to evolve (e.g., add a thread name), every call site must update.
Fix: inline to a static utility:
class Log {
public static void log(String msg) {
System.out.println("[" + Instant.now() + "] " + msg);
}
}
// Used:
Log.log("Hello");
The class is gone (well, replaced by a namespace), but formatting consistency is preserved. Inlining isn't always "delete the class"; sometimes it's "make it static."
Next: interview.md — Q&A.