Visitor Pattern Interview Questions | JiQuest

add

#

Visitor Pattern

Java design pattern deep dive

Visitor Pattern in Java: 100 interview questions with professional answers.

Learn how the Visitor pattern lets you add new operations over a stable class hierarchy without touching a single element class, how double dispatch actually resolves the right method at runtime, how the JDK itself relies on Visitor for file-tree walks and bytecode analysis, and when sealed types with pattern matching for switch are the better modern choice.

100Scenarios
4GoF roles
4Related patterns
Clientelement.accept(v) ElementA : Circleaccept(Visitor v)calls v.visit(this) ElementB : Squareaccept(Visitor v)calls v.visit(this) Visitorvisit(Circle)visit(Square) Double dispatch: the visit() overload chosen depends on both the element's AND the visitor's runtime type

What makes a good Visitor answer?

Interviewers want to see that you understand the two-hop call precisely, not just that "visitor visits things": correct double dispatch, honest acknowledgement of the Element-side cost, and judgment about when a switch statement is simply better.

Adds operations, not typesVisitor lets you add a new operation over a class hierarchy without modifying the hierarchy's classes, but adding a new Element type means updating every Visitor.
Double dispatch is the mechanismaccept() then visit() lets the right overload get chosen based on the element's concrete runtime type, not its declared, static type.
Best for stable elementsVisitor pays off when the set of element types is stable and the set of operations grows often, the opposite trade-off from a plain polymorphic method.
accept() is boilerplateEach ConcreteElement's accept() method is nearly identical across the whole hierarchy: it just calls back into the visitor with itself.
Need many new operationsover a fixed set of classeswithout touching them? Element hierarchy stable,operations grow often? Element hierarchy grows,operations stay stable? Use Visitordouble dispatch Plain polymorphicmethod is simpler
ApproachUse whenWatch out for
Classic double-dispatch Visitor (accept/visit)A stable, closed set of element types needs many operations added over time, often by different teams, without editing the element classes each time.Adding a brand-new element type forces you to touch the Visitor interface and every single ConcreteVisitor implementation.
instanceof / sealed pattern matching for switch (Java 17+)You're on a modern JDK, the element set is a sealed hierarchy, and you want compiler-enforced exhaustiveness without writing accept() boilerplate on every class.Adding a new sealed subtype still forces every exhaustive switch to be revisited; the trade-off doesn't disappear, it just moves and gets compiler-checked.
Plain polymorphic method per elementThe set of operations is small and stable, but new element subtypes are added frequently.Every new operation means touching every existing element class, and unrelated operations end up crammed into one class.
Reflection-based VisitorThe element hierarchy is large, third-party, or generated, and hand-writing accept() everywhere is impractical.Loses compile-time overload safety, adds per-call dispatch overhead, and silently no-ops on a typo'd method name instead of failing to compile.

Topics

Visitor basics Q1GoF roles Q2Double dispatch Q3 Shape hierarchy example Q4Adding RenderVisitor Q5accept() boilerplate Q6 Single dispatch bug Q7Visitor's dilemma Q8Stable vs growing trade-off Q9 Visitor vs polymorphism Q10Generic return type Q11Query vs mutating visitors Q12 Fallback visit(Object) danger Q13Supertype reference bug Q14Compile-time exhaustiveness Q15 FileVisitor walkFileTree Q16FileVisitResult control Q17ASM ClassVisitor Q18 javac TreeVisitor Q19AST expression evaluator Q20Visitor vs Iterator Q21 Visitor vs Interpreter Q22Visitor vs Composite Q23DOM export visitor Q24 DOM validation visitor Q25Visitor vs sealed+switch Q26When Visitor still wins Q27 Exhaustiveness compared Q28Migrating to pattern matching Q29Record patterns replacement Q30 Performance: dispatch vs switch Q31Generic return vs switch expr Q32Adding case vs adding subtype Q33 Hybrid sealed + accept() Q34Reflection-based Visitor Q35Testing every element type Q36 Compile-time missing overload Q37Thread safety of stateful visitor Q38Stateless vs accumulator Q39 Silent fallback bug Q40Wrong overload via supertype Q41Exhaustiveness via processor Q42 Traversal order guarantees Q43Recursive tree visitor Q44Reentrancy in accumulator Q45 Functional visitor lambdas Q46Map-based dispatch table Q47Multiple dispatch limits Q48 Visitor and OCP/LSP Q49Forgetting recursive accept Q50Visitor + Builder Q51 Visitor vs Strategy Q52Visitor vs Command Q53Encapsulation leak Q54 Getters exposed for visitor Q55Expression problem Q56Acyclic Visitor variant Q57 Reflective acyclic visitor Q58Hierarchical pre/post-order Q59Per-element generic result Q60 Void vs Visitor<R> Q61Composite chain of visitors Q62Visitor + Null Object Q63 Pretty-printing visitor Q64Tree rewriting visitor Q65Rules engine visitor Q66 GUI component visitor Q67Query AST optimizer Q68JSON/XML DOM visitor Q69 Shopping cart validation Q70Shipping cost visitor Q71Invoice tax visitor Q72 Multi-format export visitor Q73Access-control visitor Q74Spring-managed visitors Q75 Audit/logging visitor Q76Visitor + Observer Q77Performance overhead Q78 Accumulator memory concerns Q79Debugging deep call stacks Q80Documenting a Visitor API Q81 Refactoring instanceof chains Q82Annotation-processor codegen Q83Immutable result assembly Q84 Plugin architecture visitor Q85Bounded generics Q86Splitting a fat Visitor Q87 Default no-op visit() Q88Visitor vs instanceof switch Q89Exception aggregation Q90 API versioning with Visitor Q91Generics and type erasure Q92Static-analysis visitor Q93 Spreadsheet cell visitor Q94Compiler type-checking visitor Q95Parallel tree traversal Q96 Visitor anti-patterns Q97Explaining trade-offs concisely Q98Hybrid sealed + Visitor Q99 Document pipeline capstone Q100

Interview questions and answers

Each answer gives the implementation direction, the trade-off to mention, and the production concern that makes the answer stronger.

1. Explain the Visitor design pattern in Java and describe the real-world problem it solves when you need to add a new operation over a hierarchy of classes you would rather not modify.

The Visitor pattern separates an operation from the object structure it operates on. Instead of adding a new method to every class in a hierarchy every time you need a new capability, you define one Visitor interface with an overloaded visit() method per element type, and each element type gains a single accept(Visitor) method that calls back into the visitor. New operations become new ConcreteVisitor classes, and no existing element class is touched.

This solves the everyday problem of a shape hierarchy, a document object model, or a compiler's abstract syntax tree needing more and more unrelated operations, area calculation, rendering, exporting, validation, over time, where cramming every operation as a method on every element class would bloat those classes and mix unrelated concerns together.

interface Visitor {
    void visit(Circle circle);
    void visit(Square square);
}

interface Shape {
    void accept(Visitor visitor);
}

class Circle implements Shape {
    double radius;
    public void accept(Visitor visitor) { visitor.visit(this); }
}
Behavioral patternOperation externalizedNo element modification

2. Identify and describe the four classic GoF roles in the Visitor pattern (Visitor, ConcreteVisitor, Element, ConcreteElement) and explain how an ObjectStructure ties them together.

Visitor declares one overloaded visit() method per concrete element type. ConcreteVisitor implements one specific operation across every element type, for example AreaVisitor or RenderVisitor. Element declares a single accept(Visitor) method. ConcreteElement, such as Circle or Square, implements accept() to call the matching overload on the passed-in visitor, passing itself as the argument.

An ObjectStructure, often just a List<Shape> or a tree, is not a formal class in most Java implementations but is the thing that owns the elements and iterates over them, calling element.accept(visitor) for each one so the same visitor is applied uniformly across the whole collection or tree.

List<Shape> shapes = List.of(new Circle(3), new Square(4));
AreaVisitor areaVisitor = new AreaVisitor();
for (Shape shape : shapes) {
    shape.accept(areaVisitor); // object structure driving the traversal
}
System.out.println(areaVisitor.getTotalArea());

3. Explain double dispatch precisely: what does single dispatch mean in Java, and how do accept() followed by visit() together achieve a dispatch based on two runtime types instead of one?

Java is a single-dispatch language: a virtual method call is resolved at runtime based on the receiver's actual runtime type only, while the chosen overload among overloaded methods with the same name is resolved at compile time based on the static, declared types of the arguments. This is exactly why a plain call like visitor.visit(shape), where shape is statically typed as the supertype Shape, always binds to visit(Shape) at compile time, even if the actual object is a Circle.

Double dispatch fixes this with two hops. First, shape.accept(visitor) is a normal single dispatch: the JVM picks Circle.accept() or Square.accept() based on the shape's runtime type. Second, inside that concrete accept() method, the call is written as visitor.visit(this), where this now has the compile-time type Circle (because we're inside Circle's own source file), so the compiler binds it to visit(Circle). The net effect across both hops is a method chosen based on both the element's AND the visitor's runtime type.

The key insight Double dispatch isn't a JVM feature; it's a trick built entirely from two ordinary single-dispatch calls, each resolved with a different, more precise static type available at that call site.

4. Walk through a complete, worked shape-hierarchy example with Circle, Square, and Triangle elements, and two operations, an AreaVisitor and a RenderVisitor, added as separate ConcreteVisitor classes.

The element hierarchy stays fixed at three classes; each new capability is a new class implementing Visitor, and neither Circle, Square, nor Triangle is ever touched again after this is set up.

interface Visitor {
    void visit(Circle c);
    void visit(Square s);
    void visit(Triangle t);
}

interface Shape { void accept(Visitor v); }

class Circle implements Shape {
    final double radius;
    Circle(double radius) { this.radius = radius; }
    public void accept(Visitor v) { v.visit(this); }
}
class Square implements Shape {
    final double side;
    Square(double side) { this.side = side; }
    public void accept(Visitor v) { v.visit(this); }
}
class Triangle implements Shape {
    final double base, height;
    Triangle(double base, double height) { this.base = base; this.height = height; }
    public void accept(Visitor v) { v.visit(this); }
}

class AreaVisitor implements Visitor {
    double total;
    public void visit(Circle c) { total += Math.PI * c.radius * c.radius; }
    public void visit(Square s) { total += s.side * s.side; }
    public void visit(Triangle t) { total += 0.5 * t.base * t.height; }
}

class RenderVisitor implements Visitor {
    public void visit(Circle c) { System.out.println("drawing circle r=" + c.radius); }
    public void visit(Square s) { System.out.println("drawing square side=" + s.side); }
    public void visit(Triangle t) { System.out.println("drawing triangle b=" + t.base); }
}

5. Using the Circle/Square/Triangle hierarchy, demonstrate exactly what code changes when you add a brand-new operation such as a PerimeterVisitor, and confirm which files are untouched.

Adding PerimeterVisitor requires exactly one new class, nothing else. Shape, Circle, Square, Triangle, the Visitor interface, and every prior ConcreteVisitor such as AreaVisitor and RenderVisitor remain byte-for-byte unchanged.

class PerimeterVisitor implements Visitor {
    double total;
    public void visit(Circle c) { total += 2 * Math.PI * c.radius; }
    public void visit(Square s) { total += 4 * s.side; }
    public void visit(Triangle t) { /* would need side lengths, not just base/height */ }
}

// usage: no change to the object structure loop, either
for (Shape shape : shapes) shape.accept(new PerimeterVisitor());

This is the pattern's main selling point in an interview: the open/closed principle applied to operations, at the direct cost of the element hierarchy being effectively closed for extension, which the next question makes explicit.

Open for new operationsZero element-class changes

6. Explain why every ConcreteElement's accept() method looks nearly identical, and why this "boilerplate" is actually load-bearing rather than something to refactor away.

Every accept() override has the same one-line body, visitor.visit(this), and it can look like duplicated code worth eliminating. It is not duplication in the harmful sense: the single line differs meaningfully by which class it lives in, because this has a different compile-time type in each override. That compile-time type is exactly what lets the compiler pick the correct visit() overload during the second dispatch hop.

// Looks identical, but the compile-time type of "this" differs per class:
class Circle implements Shape { public void accept(Visitor v) { v.visit(this); } } // this : Circle
class Square implements Shape { public void accept(Visitor v) { v.visit(this); } } // this : Square

Trying to hoist this into one shared default method on a common interface would collapse the static type of this back to the supertype, breaking double dispatch entirely (see Q14 for exactly this bug).

7. Demonstrate the bug that occurs if you skip accept() and instead loop over a List<Shape> calling visitor.visit(shape) directly, where shape is statically typed as the Shape supertype.

If Visitor only declares overloads for concrete types (visit(Circle), visit(Square)) with no visit(Shape) overload at all, this code simply fails to compile, since no overload matches the static type Shape. If a fallback visit(Shape) overload does exist, the code compiles but every single element, regardless of whether it's actually a Circle or a Square, silently dispatches to that one generic overload, because overload resolution uses the reference's compile-time type, never the runtime type.

List<Shape> shapes = List.of(new Circle(3), new Square(4));
Visitor v = new AreaVisitor();
for (Shape shape : shapes) {
    v.visit(shape); // BUG: always resolves to visit(Shape) if it exists, never visit(Circle)/visit(Square)
}
Why this is dangerous The code compiles cleanly and runs without throwing, so this class of bug produces silently wrong results rather than a crash, which is exactly why accept() must never be skipped.

8. Explain the "Visitor's dilemma": why does adding a brand-new Element subtype force changes to the Visitor interface and every existing ConcreteVisitor, and why can this be a serious cost in practice?

Adding a new element type, say Pentagon, requires adding visit(Pentagon) to the Visitor interface. Because Java interface methods (without a default body) are abstract by default, every existing ConcreteVisitor class, AreaVisitor, RenderVisitor, PerimeterVisitor, and any others written by other teams, now fails to compile until each one is updated with an implementation for Pentagon.

In a large codebase this is a real cost: if ten teams each maintain their own ConcreteVisitor over your shared element hierarchy, adding one element type becomes a coordinated, multi-team change, which is exactly the inverse of the pattern's selling point for operations. This is why Visitor should be reserved for hierarchies where the element set is genuinely stable.

Visitor's dilemmaCoupling costDesign trade-off

9. State the core trade-off precisely: why does Visitor favor a stable set of element types with frequently changing operations, the opposite trade-off from a plain polymorphic method per element?

A plain polymorphic method, such as adding abstract double area() directly to Shape, makes adding a new element type trivial, just implement the one abstract method, but makes adding a new operation expensive, since every existing element class must gain a new method. Visitor inverts this exactly: adding an operation is one new class, but adding an element type touches the Visitor interface and every ConcreteVisitor.

New operationNew element type
Plain polymorphic methodExpensive: touch every element classCheap: one new class implementing the method
Visitor patternCheap: one new ConcreteVisitor classExpensive: touch Visitor interface + every ConcreteVisitor

Neither approach is universally better; the right choice depends entirely on which axis, elements or operations, is expected to grow more often in your specific domain.

10. Compare Visitor against simply giving each element class its own polymorphic method, with a concrete example of when the polymorphic approach is clearly the simpler, better choice.

If a shape hierarchy only ever needs one operation, area, and new shape types (Hexagon, Ellipse) are added every quarter while no second operation is ever likely, a plain abstract double area() on Shape is simpler: no Visitor interface, no accept() boilerplate, and adding a shape is a single self-contained class with no coordination cost across other files.

// Simpler when there's really only one stable operation and elements keep growing:
abstract class Shape { abstract double area(); }
class Hexagon extends Shape { double area() { /* ... */ return 0; } }

Reach for Visitor only once a second or third genuinely independent operation appears, or once you know in advance that the element set is closed but the operations are not.

11. How do you design a generic Visitor<R> interface whose visit() methods return a typed result R, rather than performing void side effects, and what changes on the Element side?

Parameterize the Visitor interface with a type variable R, declare each visit() method to return R, and change accept() to also be generic and to return whatever the visitor's visit() call returns, threading the result straight back to the caller instead of accumulating it in a mutable field.

interface Visitor<R> {
    R visit(Circle c);
    R visit(Square s);
}

interface Shape {
    <R> R accept(Visitor<R> visitor);
}

class Circle implements Shape {
    double radius;
    public <R> R accept(Visitor<R> visitor) { return visitor.visit(this); }
}

class AreaVisitor implements Visitor<Double> {
    public Double visit(Circle c) { return Math.PI * c.radius * c.radius; }
    public Double visit(Square s) { return s.side * s.side; }
}

double area = shape.accept(new AreaVisitor());
Generic Visitor<R>Functional-style return

12. Distinguish a read-only query Visitor, such as one computing total area, from a mutating Visitor that changes element state during traversal, and discuss the risks of the latter.

A query visitor only reads state off each element and accumulates or returns a result; it is safe to run repeatedly, safe to run concurrently against read-only elements, and easy to reason about. A mutating visitor, for example one that recalculates and writes a cached bounding-box field back onto each shape during traversal, changes the object structure itself as a side effect of "just visiting it," which can surprise other code that assumed visiting was inert.

Design guidance Prefer read-only visitors by default, and name mutating ones explicitly, such as RecalculateBoundsVisitor, so their side effect is obvious from the call site rather than discovered by debugging.

13. Explain the danger of adding a catch-all default visit(Object o) overload to the Visitor interface as a fallback, and why the compiler will not catch a missing element-specific overload once it exists.

A default visit(Object o) method (or overload) is sometimes added so a ConcreteVisitor "compiles even if it doesn't handle every element type yet." The problem is that this silently defeats the entire benefit of an abstract, exhaustive Visitor interface: once a fallback exists, forgetting to implement visit(Pentagon) after adding the Pentagon element type no longer produces a compile error, it just quietly routes every Pentagon through the generic fallback at runtime.

interface Visitor {
    default void visit(Object o) { /* dangerous: swallows unhandled types silently */ }
    void visit(Circle c);
    void visit(Square s);
}
// PentagonVisitor "compiles" but Pentagon quietly falls through to visit(Object) forever
Rule of thumb Never add a generic fallback overload to a Visitor interface meant to be exhaustive; the whole point of the interface is that the compiler forces every ConcreteVisitor to handle every element explicitly.

14. Walk through a concrete bug where an accept() method is defined on a common superclass and mistakenly calls visitor.visit(this) with this resolved to the supertype instead of the concrete subtype.

Double dispatch depends on this having the most specific compile-time type available at the call site. If a well-meaning refactor hoists accept() up into an abstract base class, hoping to eliminate the "boilerplate" from Q6, the compiler now sees this with the base class's static type inside that shared method, and every subclass's call resolves to the base-class overload (or fails to compile if none exists), regardless of which concrete subclass actually invoked it.

abstract class Shape {
    // BUG: this is statically typed as Shape here, not Circle or Square
    public void accept(Visitor v) { v.visit(this); } // always binds to visit(Shape), if it exists
}
class Circle extends Shape { /* no override: inherits the broken accept() */ }

The fix is exactly what Q6 explained: accept() must be overridden individually in each concrete class, never inherited from a shared base implementation, precisely so each override's this carries that class's own compile-time type.

15. How does the Visitor pattern give you compile-time exhaustiveness checking for free, and how would you deliberately break that guarantee by mistake?

Because Visitor's visit() methods are ordinary abstract interface methods, any class declaring implements Visitor is forced by the Java compiler to implement every single one of them, or the class must itself be declared abstract. This means the compiler, not a code reviewer, catches the case where a new ConcreteVisitor forgets to handle one of the existing element types.

You break this guarantee, as Q13 showed, by adding a default method to the interface, since default methods are optional to override. You can also break it more subtly by having ConcreteVisitor extend an intermediate abstract class that itself provides empty or no-op implementations "to save typing" (the AWT WindowAdapter idiom, discussed in Q88), which reintroduces exactly the same silent-fallback risk in a different shape.

16. Explain how java.nio.file.FileVisitor and Files.walkFileTree exemplify the Visitor pattern in the JDK, with a worked example that counts files by extension across a directory tree.

FileVisitor<T> is a real, very commonly used Visitor in the JDK: it declares four callback methods, preVisitDirectory, visitFile, visitFileFailed, and postVisitDirectory, and Files.walkFileTree is the object structure that walks the directory tree, invoking the appropriate callback for every directory and file it encounters.

Map<String, Integer> countsByExtension = new HashMap<>();
Files.walkFileTree(Path.of("src"), new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        String name = file.getFileName().toString();
        int dot = name.lastIndexOf('.');
        String ext = dot >= 0 ? name.substring(dot + 1) : "(none)";
        countsByExtension.merge(ext, 1, Integer::sum);
        return FileVisitResult.CONTINUE;
    }
});
JDK VisitorFile-tree traversal

17. Explain the FileVisitResult enum returned from each FileVisitor callback, and how CONTINUE, SKIP_SUBTREE, SKIP_SIBLINGS, and TERMINATE let the visitor control the traversal itself, unlike a typical Element/Visitor pair.

Most classic Visitor implementations leave traversal entirely to the object structure and never let the visitor influence it. FileVisitor is a more powerful variant: each callback's return value tells Files.walkFileTree exactly how to proceed. CONTINUE walks normally, SKIP_SUBTREE (returned from preVisitDirectory) skips an entire directory's contents, SKIP_SIBLINGS skips remaining entries at the current level, and TERMINATE stops the whole walk immediately.

@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
    if (dir.getFileName().toString().equals("node_modules")) {
        return FileVisitResult.SKIP_SUBTREE; // visitor steers the traversal
    }
    return FileVisitResult.CONTINUE;
}

18. Describe how ASM's ClassVisitor and MethodVisitor use the Visitor pattern to let bytecode-manipulation tools process and rewrite compiled class files without hand-parsing the class file format.

ASM models a compiled .class file as an object structure of classes, fields, and methods, and exposes a ClassVisitor whose methods, visitField, visitMethod, visitAnnotation, are called by ASM's reader as it parses the bytecode. visitMethod itself returns a MethodVisitor, which then receives a further stream of callbacks, visitInsn, visitVarInsn, for every individual bytecode instruction inside that method.

ClassVisitor logger = new ClassVisitor(Opcodes.ASM9) {
    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("visiting method: " + name + descriptor);
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }
};
new ClassReader(classBytes).accept(logger, 0); // classic accept()/visitor call

Tools built on this, coverage instrumenters, AOP frameworks, and profilers, layer new ConcreteVisitors on top without ASM's own reader/writer classes ever changing, the same operations-added-freely benefit as Q5's shape example, just applied to bytecode.

19. Explain how the Java compiler's own com.sun.source.tree API uses a TreeVisitor to let annotation processors and static analysis tools traverse a source file's abstract syntax tree.

The compiler tree API models a parsed Java source file as a tree of Tree nodes (ClassTree, MethodTree, BinaryTree, and dozens more), and TreeVisitor<R,P> declares one visitXxx method per node kind. A base class, SimpleTreeVisitor, provides sensible defaults so a custom visitor, used inside an annotation processor, can override only the node kinds it actually cares about.

class UnusedImportScanner extends SimpleTreeVisitor<Void, Void> {
    @Override
    public Void visitImport(ImportTree node, Void unused) {
        // record the import, later cross-check against actual usage
        return null;
    }
}

This is the same shape as ASM's ClassVisitor: a large, closed set of node types (Java grammar rarely changes) with an ever-growing set of operations, lint rules, refactoring tools, code generators, each shipped as its own visitor.

20. Design an AST-based expression evaluator for a small arithmetic language using the Visitor pattern, with node types for literals, addition, and multiplication.

Model each grammar rule as an element type implementing a common Expr interface, then write an EvalVisitor that recursively evaluates each subtree, calling accept() on child nodes rather than re-implementing tree-walking logic inside every node class.

interface Expr { <R> R accept(ExprVisitor<R> visitor); }
interface ExprVisitor<R> {
    R visit(Literal literal);
    R visit(Add add);
    R visit(Multiply multiply);
}

class Literal implements Expr {
    final double value;
    Literal(double value) { this.value = value; }
    public <R> R accept(ExprVisitor<R> v) { return v.visit(this); }
}
class Add implements Expr {
    final Expr left, right;
    Add(Expr left, Expr right) { this.left = left; this.right = right; }
    public <R> R accept(ExprVisitor<R> v) { return v.visit(this); }
}

class EvalVisitor implements ExprVisitor<Double> {
    public Double visit(Literal literal) { return literal.value; }
    public Double visit(Add add) { return add.left.accept(this) + add.right.accept(this); }
    public Double visit(Multiply multiply) { return multiply.left.accept(this) * multiply.right.accept(this); }
}

Adding a second operation, a PrettyPrintVisitor that renders the expression back to a string, requires zero changes to Literal, Add, or Multiply, exactly the payoff from Q5.

21. Compare the Visitor pattern to the Iterator pattern precisely: what does each one externalize, and how are they commonly combined in the same traversal?

Iterator externalizes traversal: it lets a client step through a collection's elements one at a time without exposing the collection's internal structure, but the client still decides what to do with each element. Visitor externalizes the operation performed at each element: the object structure (or an Iterator walking it) still controls which element comes next, but the operation itself is defined once, in a Visitor class, and reused across every element uniformly.

Iterator<Shape> it = shapes.iterator(); // Iterator: controls traversal
Visitor operation = new AreaVisitor();  // Visitor: controls what happens at each stop
while (it.hasNext()) {
    it.next().accept(operation);
}

They are frequently combined exactly like this: an Iterator (or a tree-walking loop) drives the "what's next" question, while a Visitor is applied at each stop to answer the "what do we do here" question, cleanly separating two orthogonal concerns.

22. Compare the Visitor pattern to the Interpreter pattern: why is an interpreter's abstract syntax tree frequently evaluated using a Visitor rather than giving every AST node its own interpret() method?

The classic GoF Interpreter pattern puts an interpret(Context) method directly on every expression class, which is simple for a tiny, stable grammar but suffers exactly the polymorphic-method cost from Q9 as the language grows: adding a second interpretation mode, say a type-checker alongside the evaluator, means touching every single node class again.

In practice, most non-trivial interpreters (and the JDK compiler's tree API from Q19 is a real example) instead give each AST node type a bare accept() method and implement evaluation, pretty-printing, type-checking, and constant-folding as separate Visitor classes, because a language's grammar (the element types) is comparatively stable while the number of passes performed over it (the operations) tends to grow throughout a compiler or interpreter's lifetime.

Interpreter's ASTMultiple compiler passes

23. Compare the Visitor pattern to the Composite pattern: how does a Visitor traverse every node of a Composite tree, and how must it handle composite (branch) nodes differently from leaf nodes?

Composite gives you a uniform tree of nodes, some of which are leaves and some of which contain children, addressed through one common component interface. Visitor is very frequently layered on top of a Composite tree specifically to avoid an instanceof chain inside client code: each node type, leaf and composite alike, gets an accept() method, but a composite node's accept() must also recurse into its children, while a leaf's does not.

class Leaf implements FileSystemNode {
    public void accept(Visitor v) { v.visit(this); }
}
class Folder implements FileSystemNode {
    List<FileSystemNode> children;
    public void accept(Visitor v) {
        v.visit(this);                                  // visit the composite itself
        for (FileSystemNode child : children) child.accept(v); // then recurse into children
    }
}

The visitor implementing an operation such as "sum all file sizes" never needs to know whether it is looking at a leaf or a branch beyond the two overloads it implements; the recursion responsibility lives entirely inside each composite's own accept().

24. Design a SerializationVisitor that exports an in-memory document-object-model tree (Paragraph, Table, Image nodes) to a single output format, without adding an export method to any node class.

Each DOM node type implements accept(DocVisitor), and a JsonExportVisitor implements the operation once, building up an output buffer as it visits each node kind, recursing into composite nodes such as Table the way Q23 described.

class JsonExportVisitor implements DocVisitor {
    private final StringBuilder out = new StringBuilder();
    public void visit(Paragraph p) { out.append("{\"type\":\"p\",\"text\":\"").append(p.text()).append("\"},"); }
    public void visit(Image img) { out.append("{\"type\":\"img\",\"src\":\"").append(img.src()).append("\"},"); }
    public void visit(Table table) {
        out.append("{\"type\":\"table\",\"rows\":[");
        for (Row row : table.rows()) row.accept(this);
        out.append("]},");
    }
    String result() { return out.toString(); }
}

A later requirement to also support XML export becomes one new XmlExportVisitor class, again with no change to Paragraph, Table, or Image.

25. Design a ValidationVisitor that checks structural and business rules across the same document-object-model hierarchy from Q24, and explain how it can share the tree with the export visitor safely.

ValidationVisitor implements the same DocVisitor interface, accumulating a list of violations rather than building output text, and it can be run over the identical tree that JsonExportVisitor processes, since both are read-only with respect to the DOM's own fields (see Q12).

class ValidationVisitor implements DocVisitor {
    private final List<String> violations = new ArrayList<>();
    public void visit(Paragraph p) { if (p.text().isBlank()) violations.add("empty paragraph"); }
    public void visit(Image img) { if (img.src().isBlank()) violations.add("image missing src"); }
    public void visit(Table table) { for (Row row : table.rows()) row.accept(this); }
    List<String> violations() { return violations; }
}

Because both visitors only read node state, running validation before export, or interleaving them across two separate traversals, is safe with no coordination required between the two ConcreteVisitor classes.

26. Compare the classic double-dispatch Visitor pattern to Java 17/21's sealed interfaces combined with pattern matching for switch, using the Circle/Square/Triangle example rewritten both ways.

A sealed interface declares its permitted subtypes closed at compile time, and a switch expression using pattern matching over that sealed type lets the compiler verify exhaustiveness directly, without any accept()/visit() machinery at all.

sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double radius) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}

double area(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square s -> s.side() * s.side();
        case Triangle t -> 0.5 * t.base() * t.height();
        // no default needed: compiler proves this switch is exhaustive over the sealed type
    };
}
Java 17+ sealed typesPattern matching for switchNo accept()/visit() boilerplate

27. Given how convenient sealed types plus pattern matching for switch are, explain precisely when the classic Visitor pattern is still the better choice in a modern Java codebase.

Visitor still earns its place when operations are contributed by separate, independently compiled modules or plugins that cannot see or modify each other's switch statements, when an operation itself needs to carry substantial mutable state or dependencies across a traversal (naturally modeled as fields on a ConcreteVisitor instance), or when the element hierarchy is not sealed, not closed, and not entirely under your control, such as classes coming from a third-party library or a hierarchy meant to remain open for external extension.

Sealed types plus switch wins when you own the whole hierarchy, can afford to close it, are targeting Java 17+ (21+ for the nicest record-pattern ergonomics), and want to avoid writing accept() boilerplate on every element class purely to enable a mechanism the language now provides natively.

28. Compare how Visitor achieves exhaustiveness checking (via the compiler forcing every abstract visit() method to be implemented) against how a sealed-type switch achieves it (via the compiler verifying every permitted subtype has a case).

Visitor's exhaustiveness guarantee is anchored on the ConcreteVisitor side: the compiler forces every class that implements the Visitor interface to handle every visit() overload, so each operation is independently checked. A sealed-type switch's exhaustiveness guarantee is anchored on the switch expression itself: the compiler checks that a given switch covers every permitted subtype of the sealed interface, so each individual switch statement is checked at the point it is written.

What's checkedChecked when
VisitorEvery ConcreteVisitor implements every visit() overloadWhen each ConcreteVisitor class is compiled
Sealed + switchEvery switch covers every permitted subtypeWhen each switch expression is compiled

Both mechanisms genuinely prevent forgetting a case at compile time; they simply differ in which artifact, the visitor class or the switch expression, carries that compiler-enforced obligation.

29. Walk through migrating a legacy Visitor-based shape hierarchy to sealed interfaces and pattern matching for switch, step by step, and note what risk this migration carries.

First, convert each ConcreteElement class to a record implementing a new sealed interface Shape permits ..., dropping the accept() method entirely. Second, for each existing ConcreteVisitor, rewrite its logic as a method taking a Shape parameter and returning the same result via an exhaustive switch, one case per permitted subtype, in place of the old overloaded visit() methods. Third, update every call site that used to call shape.accept(visitor) to instead call the new plain method directly, for example areaOf(shape).

// before
class AreaVisitor implements Visitor { /* visit(Circle), visit(Square), ... */ }
shape.accept(areaVisitor);

// after
double areaOf(Shape shape) {
    return switch (shape) {
        case Circle c -> Math.PI * c.radius() * c.radius();
        case Square s -> s.side() * s.side();
    };
}
areaOf(shape);
Migration risk This only works cleanly if the element set is genuinely closed going forward; if any external code is expected to add new Shape implementations later, sealing the interface actively forbids that, unlike the open Visitor interface it replaces.

30. Explain how Java 21's record patterns let a pattern-matching switch destructure nested element state directly in the case label, further reducing the need for a Visitor-style getter surface.

Because Q4's elements are records, Java 21 record patterns let a case label both match the type and immediately bind its components as local variables, avoiding calls like c.radius() inside the case body, and this composes with nested records too.

record Point(double x, double y) {}
record Circle(Point center, double radius) implements Shape {}

double distanceFromOrigin(Shape shape) {
    return switch (shape) {
        case Circle(Point(var x, var y), var radius) -> Math.hypot(x, y);
        case Square s -> 0.0;
        default -> 0.0;
    };
}

This further narrows the gap that Visitor used to fill: destructuring nested state that would otherwise need several chained getter calls (and the encapsulation questions raised in Q54/Q55) is now built into the switch syntax itself.

31. Compare the runtime performance of Visitor's virtual-dispatch-based double dispatch against a pattern-matching switch over a sealed type, and explain what the JIT compiler can and cannot optimize in each case.

Visitor's two hops, accept() then visit(), are both ordinary virtual method calls; a JIT compiler that sees a call site repeatedly hit the same concrete type (monomorphic) will typically inline and devirtualize both, making the steady-state cost close to a direct call. A pattern-matching switch over a sealed type typically compiles down to a type-test-and-branch sequence (conceptually similar to a chain of instanceof checks, though the compiler can sometimes use more efficient dispatch depending on the sealed hierarchy's shape), which the JIT can likewise optimize well once a branch pattern stabilizes.

In practice neither approach shows a measurable difference for typical business logic; the difference only becomes visible in extremely hot loops, and even then should be confirmed with a proper JMH benchmark (see Q78) rather than assumed from first principles.

32. Compare Visitor's generic Visitor<R> return-type mechanism (Q11) against a switch expression's inline return value, in terms of code that a new team member has to read to understand what a call produces.

A switch expression's return type and every branch's value are visible in one place, directly at the call site's containing method, making it immediately obvious what a given call computes. A Visitor<R> call, shape.accept(new AreaVisitor()), requires jumping into the AreaVisitor class definition to see what each visit() overload actually returns, one extra hop of indirection that a switch expression avoids entirely.

// switch expression: everything visible in one place
double area = switch (shape) { case Circle c -> ...; case Square s -> ...; };

// Visitor: must open AreaVisitor.java to see what's actually computed
double area = shape.accept(new AreaVisitor());

33. Does moving from Visitor to sealed types plus pattern matching for switch actually eliminate the Visitor's dilemma from Q8, or does the same fundamental trade-off just move somewhere else?

The trade-off does not disappear, it relocates. With classic Visitor, adding a new element type breaks every ConcreteVisitor class, each a separate file. With a sealed interface and pattern-matching switches, adding a new permitted subtype breaks every exhaustive switch statement written over that sealed type, wherever in the codebase they live, which the compiler flags in exactly the same spirit as the old "must implement every visit() overload" rule.

What genuinely changes is ergonomics, not the trade-off itself: no accept() boilerplate, and switches over a sealed type are ordinary methods discoverable by any IDE's "find usages" on the sealed interface, arguably easier to locate than scattered ConcreteVisitor classes implementing an interface.

Same trade-off, different shapeCompiler-enforced either way

34. Describe a hybrid design where an element hierarchy is both a sealed interface AND retains an accept() method, and explain what scenario would justify paying for both mechanisms at once.

A hierarchy can be sealed (so internal code gets exhaustive-switch safety) while still implementing an accept(Visitor) method (so external plugin code, which cannot add permitted subtypes to your sealed interface anyway, can still register new operations via a Visitor without needing access to modify your switches). This is worth doing when your own team wants switch-based exhaustiveness for internal logic, but you also expose the hierarchy to external consumers as a plugin extension point for operations.

sealed interface Shape permits Circle, Square {
    void accept(Visitor visitor); // still supports externally-authored operations
}
// internal code can still use: switch (shape) { case Circle c -> ...; case Square s -> ...; }
// external plugins implement their own Visitor without touching Shape at all

35. Explain how to implement a reflection-based Visitor that dispatches to a matching visitXxx method by inspecting the element's class name at runtime, and what trade-offs this incurs versus hand-written overloads.

Instead of declaring one overload per element type, a reflective visitor looks up a method named, by convention, "visit" + element.getClass().getSimpleName() via getClass().getMethod(...) and invokes it dynamically. This is sometimes used when the element hierarchy is very large, third-party, or generated, and hand-declaring dozens of overloads is impractical.

void dispatch(Object element) {
    try {
        Method method = getClass().getMethod("visit" + element.getClass().getSimpleName(), element.getClass());
        method.invoke(this, element);
    } catch (NoSuchMethodException e) {
        throw new IllegalStateException("No visit method for " + element.getClass(), e);
    } catch (ReflectiveOperationException e) {
        throw new RuntimeException(e);
    }
}
Trade-off A typo in the method name, or a missing overload, becomes a runtime exception instead of a compile error, exactly reintroducing the exhaustiveness risk from Q13 and Q15 that the hand-written Visitor interface was designed to prevent.

36. Explain how to write unit tests for a ConcreteVisitor that assert correct results across every ConcreteElement type in the hierarchy, and why testing only one element type is an incomplete test suite.

Because a ConcreteVisitor's logic is spread across one visit() overload per element type, each overload is effectively an independent unit of behavior and deserves its own test case; testing only Circle gives you zero coverage of whatever the Square or Triangle overload actually does.

@Test void computesCircleArea() {
    assertEquals(Math.PI * 4, new AreaVisitor().let(v -> { new Circle(2).accept(v); return v.total; }), 0.001);
}
@Test void computesSquareArea() {
    AreaVisitor v = new AreaVisitor();
    new Square(3).accept(v);
    assertEquals(9.0, v.total, 0.001);
}
@Test void computesTriangleArea() {
    AreaVisitor v = new AreaVisitor();
    new Triangle(4, 5).accept(v);
    assertEquals(10.0, v.total, 0.001);
}

37. How does the Visitor pattern let you catch a missing visit() overload for a new element type at compile time, and demonstrate the compiler error a teammate would see if they forget one?

As long as the Visitor interface declares only abstract overloads (no default fallback, per Q13), the moment a new element type's overload is added to the interface, every ConcreteVisitor class that does not implement it fails to compile with an error to the effect of "AreaVisitor is not abstract and does not override abstract method visit(Pentagon) in Visitor."

// after adding visit(Pentagon) to the Visitor interface:
// error: AreaVisitor is not abstract and does not override abstract method visit(Pentagon) in Visitor
class AreaVisitor implements Visitor { /* ... missing visit(Pentagon) ... */ }

This is the practical value of Q15's exhaustiveness guarantee: the build simply breaks everywhere it needs to, immediately, rather than the gap being discovered later by a failing test or a wrong result in production.

38. Discuss the thread-safety implications of sharing one Visitor instance that accumulates mutable state, such as a running total, across a traversal that multiple threads might invoke concurrently.

A stateful visitor like AreaVisitor, whose total field is mutated by every visit() call, is not safe to share across threads unless each field access is properly synchronized; two threads calling accept() on different elements against the same shared visitor instance can race on the same mutable field, corrupting the accumulated result.

// UNSAFE if shared across threads without synchronization:
class AreaVisitor implements Visitor {
    double total; // plain field, racy under concurrent visit() calls
}

// SAFER: give each thread (or each traversal) its own instance
Supplier<AreaVisitor> factory = AreaVisitor::new;
shapes.parallelStream().forEach(s -> s.accept(factory.get())); // still needs a merge step
Simplest fix Prefer one fresh visitor instance per traversal (or per thread) over sharing a single mutable visitor, and merge partial results afterward if the traversal itself is parallelized.

39. Contrast a stateless Visitor whose visit() methods only compute and return values with a stateful Visitor that accumulates results in an instance field, and explain when each style is safer to reuse.

A stateless visitor, one built around Visitor<R> from Q11 with no instance fields at all, is trivially safe to share as a singleton across any number of threads or traversals, since there is no mutable state to race on; every call is independent. A stateful visitor with an accumulator field is inherently tied to one traversal's lifetime and must be freshly constructed (or explicitly reset) before each use.

Prefer the stateless, return-value style whenever the operation naturally produces one value per element with no cross-element aggregation needed; reserve stateful accumulator visitors for genuinely aggregate operations like a running total or a collected list, and treat each such visitor instance as single-use.

40. Walk through the exact bug where a new Element type is added, an existing ConcreteVisitor is not updated, and the mistake is only caught at runtime because of a fallback visit(Object) overload the compiler didn't flag.

This chains Q8's dilemma with Q13's fallback danger: suppose Pentagon is added and the Visitor interface gains visit(Pentagon), but because a default void visit(Object o) {} fallback already exists on the interface for "convenience," AreaVisitor compiles fine without ever implementing visit(Pentagon). Every Pentagon in the shape list silently contributes zero to the total area, with no compiler warning and no exception.

interface Visitor {
    default void visit(Object o) {} // the trap
    void visit(Circle c);
    void visit(Square s);
    void visit(Pentagon p); // added later
}
class AreaVisitor implements Visitor {
    double total;
    public void visit(Circle c) { total += Math.PI * c.radius * c.radius; }
    public void visit(Square s) { total += s.side * s.side; }
    // visit(Pentagon) missing -- compiles fine because of the fallback, silently under-counts area
}

This bug is only found by a test that specifically exercises every element type (Q36), which is exactly why fallback overloads are discouraged and why exhaustiveness matters more than convenience here.

41. Explain a subtler variant of the dispatch bug: an accept() method is called correctly through virtual dispatch, but a caller elsewhere directly invokes visitor.visit(element) with element statically typed as a supertype, choosing the wrong overload.

Calling element.accept(visitor) is always safe regardless of the reference's static type, because accept() itself is an ordinary virtual method resolved by the JVM based on runtime type (single dispatch), and the concrete class's own accept() override supplies the precisely-typed this for the second hop. The bug from Q7 only appears when code bypasses accept() and calls visitor.visit(element) directly, since that expression's overload resolution happens at compile time based on element's declared type at that specific call site, not its runtime type.

Shape shape = new Circle(3); // static type Shape, runtime type Circle
shape.accept(visitor);       // SAFE: always resolves correctly via double dispatch
visitor.visit(shape);        // BUG (if it even compiles): resolves to visit(Shape) at compile time, ignoring that shape is really a Circle
Static vs runtime typeAlways call accept(), never visit() directly

42. Describe how an annotation processor or a custom static-analysis rule could catch, at build time, a ConcreteVisitor that silently relies on a fallback overload instead of implementing every element-specific visit() method.

Since the compiler alone cannot flag this once a default fallback exists (Q40), a custom build-time check can: an annotation processor can enumerate every permitted element type (for example via a @VisitorFor(Circle.class, Square.class, Pentagon.class) annotation on the Visitor interface) and, for each class implementing it, verify via the processing environment's type-checking APIs that a same-signature, non-default visit() method exists for every listed type, failing the build otherwise.

This restores the exhaustiveness guarantee that a plain fallback method quietly removed, at the cost of maintaining the annotation processor itself; teams that hit this problem often decide it's simpler to just avoid fallback overloads in the first place (Q13's rule of thumb).

43. What ordering guarantees, if any, does the Visitor pattern provide about the sequence in which elements are visited, and how is traversal order actually controlled?

The Visitor pattern itself makes no promise about ordering at all; a Visitor only defines what happens once visit() is called for a given element. The actual sequence, first shape, then which, then which, is entirely controlled by whatever iterates the object structure, a plain for loop over a List, a recursive walk over a tree (Q23, Q44), or a JDK mechanism like Files.walkFileTree (which does document its own specific ordering).

When order matters, for example a rendering visitor that must draw background shapes before foreground shapes, that constraint belongs to the object structure's iteration logic (or to an explicitly sorted input list), not to the Visitor class itself.

44. Design a recursive Visitor for a tree-shaped structure, such as a file system, that must visit every node and correctly recurse into children, contrasted with the flat list traversal used in the shape examples.

Unlike the flat List<Shape> examples where an external loop drives the traversal, a tree structure needs the recursion to live inside each composite node's own accept() method, exactly as Q23 described, so the visitor itself stays free of tree-walking logic and only implements what happens at each node kind.

interface FsVisitor { void visit(FileNode file); void visit(FolderNode folder); }

class FolderNode implements FsNode {
    List<FsNode> children;
    public void accept(FsVisitor v) {
        v.visit(this);
        for (FsNode child : children) child.accept(v); // recursion lives here, not in the visitor
    }
}

class TotalSizeVisitor implements FsVisitor {
    long total;
    public void visit(FileNode file) { total += file.size(); }
    public void visit(FolderNode folder) { /* nothing extra: children are visited via accept() recursion */ }
}

45. Discuss reentrancy risks when a stateful, accumulating Visitor is used with a recursive tree traversal that calls back into the same visitor instance for nested elements, particularly if a visit() method itself triggers further accept() calls.

If a visit() method's own logic calls accept() on other elements (rather than relying purely on the composite's own recursive accept(), as in Q44), the visitor re-enters itself while a mutation from the outer call is still in progress, which is fine for simple accumulator fields like a running total but becomes fragile the moment the visitor maintains ordered state, such as a stack tracking the current nesting depth, that assumes strict push/pop symmetry around each accept() call.

class DepthTrackingVisitor implements FsVisitor {
    int depth = 0;
    public void visit(FolderNode folder) {
        depth++;
        for (FsNode child : folder.children()) child.accept(this); // reentrant call
        depth--; // must be perfectly balanced with the increment above
    }
}
Watch out An exception thrown mid-traversal can skip the decrement above, leaving depth permanently wrong if the same visitor instance is reused; wrap such balanced state changes in try/finally.

46. Show how to implement a functional-style Visitor in modern Java using per-type lambdas passed to accept(), instead of a hand-written class implementing an interface with multiple overloads.

Rather than requiring a whole named class per operation, accept() can take a small set of Function parameters, one per element type, letting a caller supply the logic inline as lambdas for quick, one-off operations without ceremony.

interface Shape {
    <R> R accept(Function<Circle, R> onCircle, Function<Square, R> onSquare);
}
class Circle implements Shape {
    public <R> R accept(Function<Circle, R> onCircle, Function<Square, R> onSquare) { return onCircle.apply(this); }
}
class Square implements Shape {
    public <R> R accept(Function<Circle, R> onCircle, Function<Square, R> onSquare) { return onSquare.apply(this); }
}

double area = shape.accept(c -> Math.PI * c.radius * c.radius, s -> s.side * s.side);

This trades the discoverability and reusability of a named ConcreteVisitor class for brevity at the call site, and becomes unwieldy once the element hierarchy grows past three or four types, since every accept() call site must supply a lambda for each one.

47. Describe an alternative to interface-based double dispatch using a Map<Class<?>, Function<?,?>> dispatch table keyed by element class, and its trade-offs against the classic Visitor interface.

Instead of an overloaded visit() method per type, a dispatch-table visitor holds a Map from each element's Class object to a handler function, and looks up the right handler at accept() time by calling element.getClass(), avoiding both accept()'s double-dispatch machinery and reflection's method-name lookup from Q35.

class DispatchTableVisitor {
    private final Map<Class<?>, Function<Object, Double>> handlers = Map.of(
        Circle.class, o -> { Circle c = (Circle) o; return Math.PI * c.radius * c.radius; },
        Square.class, o -> { Square s = (Square) o; return s.side * s.side; }
    );
    double compute(Object element) {
        Function<Object, Double> handler = handlers.get(element.getClass());
        if (handler == null) throw new IllegalArgumentException("No handler for " + element.getClass());
        return handler.apply(element);
    }
}
Trade-off Like reflection, a missing entry is a runtime IllegalArgumentException rather than a compile error, and every handler needs an unchecked cast, losing the type safety that made the classic Visitor worth its boilerplate.

48. Explain why the Visitor pattern is limited to two-way double dispatch and describe the practical difficulty of extending it to true multiple dispatch across three or more independently-varying types.

Visitor achieves dispatch on two axes, the element's type and the visitor's type, by having each side supply one precisely-typed hop. Extending this to a third independent axis, for example an operation whose behavior must also vary by a separate "rendering surface" type (screen versus printer versus SVG export), requires either a visitor per (element type, surface type) pair, exploding combinatorially, or a visitor whose visit() methods themselves take the third parameter and internally branch on it, which reintroduces the instanceof-style branching Visitor was meant to avoid.

In practice, true N-way multiple dispatch is rarely implemented cleanly in Java at all; most real systems instead pick one axis (usually the element type) for double dispatch via Visitor and handle any remaining axis through ordinary polymorphism, configuration, or explicit branching on the smaller, more stable dimension.

49. Relate the Visitor pattern to the Open/Closed Principle and the Liskov Substitution Principle: which principle does it directly serve, and where does it create tension with the other?

Visitor is a textbook mechanism for the Open/Closed Principle on the operations axis: the element hierarchy is closed for modification while the set of operations stays open for extension, exactly as Q5 demonstrated. It creates tension with Liskov substitution only indirectly, through the accept() boilerplate itself: if a subclass's accept() override fails to call visitor.visit(this) with its own precisely-typed this (as in Q14's bug), substituting that subclass for its supertype silently changes behavior in a way LSP is meant to rule out, even though the method signature itself is perfectly compatible.

OCP on operationsLSP risk via broken accept()

50. Summarize the most common real-world bug in Visitor-based code involving composite structures: forgetting to recursively call accept() on child elements inside a composite node's own accept() implementation.

Recalling Q23 and Q44, a composite node's accept() must both call visitor.visit(this) for itself AND loop over its children calling child.accept(visitor) for each. The most common bug in tree-shaped Visitor code is simply forgetting the second half, so the visitor "sees" every top-level composite node but silently never descends into its children.

class FolderNode implements FsNode {
    List<FsNode> children;
    public void accept(FsVisitor v) {
        v.visit(this);
        // BUG: forgot the loop below -- children are silently never visited
        // for (FsNode child : children) child.accept(v);
    }
}

Because the code compiles and runs without error, producing merely an undercount rather than a crash, this bug is best caught with a test asserting the visitor's result across a tree with at least two levels of nesting, not just a single flat level (see Q36 for the analogous flat-hierarchy test discipline).

51. Describe combining Visitor with Builder, where a Visitor traverses an element tree and incrementally assembles a complex result object via a Builder rather than a simple accumulator field.

When the result of a traversal is a rich object, not just a number or a string, a ConcreteVisitor can hold a Builder instance internally and call its step-by-step methods as it visits each element, letting the builder handle validation and construction order while the visitor focuses purely on deciding which builder step corresponds to which element type.

class ReportBuildingVisitor implements DocVisitor {
    private final ReportBuilder builder = new ReportBuilder();
    public void visit(Paragraph p) { builder.addTextSection(p.text()); }
    public void visit(Image img) { builder.addImageSection(img.src()); }
    Report result() { return builder.build(); }
}

52. Compare the Visitor pattern to the Strategy pattern: both encapsulate an algorithm as an object, so what structurally distinguishes them?

Strategy encapsulates one interchangeable algorithm behind a single-method interface, and a context object holds and delegates to exactly one strategy at a time, chosen without regard to the type of any particular element. Visitor encapsulates an operation that must behave differently depending on which of several element types it's applied to, via multiple overloaded methods, one per type, resolved through double dispatch rather than through a single delegated call.

Method shapeVaries by
StrategyOne method, e.g. execute()Which strategy object is plugged in
VisitorMany overloaded visit() methodsWhich element type accept() is called on

53. Compare the Visitor pattern to the Command pattern: how do they differ in what gets bundled, and can they be combined?

Command bundles a single request (and its parameters) as an object so it can be queued, logged, or undone, independent of what invokes it or receives it. Visitor bundles a family of type-specific behaviors as one object applied across a whole hierarchy of receivers. They combine naturally when each concrete visit() operation is itself represented as an undoable Command, letting a traversal both apply an operation per element and record each step onto an undo stack.

class UndoableRenameVisitor implements FsVisitor {
    private final Deque<Command> history = new ArrayDeque<>();
    public void visit(FileNode file) {
        Command rename = new RenameCommand(file, file.name() + "_backup");
        rename.execute();
        history.push(rename); // Command captured per visited element
    }
}

54. Discuss how the Visitor pattern can force ConcreteElement classes to expose more public state (getters) than they otherwise would, and why this is considered a genuine encapsulation cost of the pattern.

Because a ConcreteVisitor lives outside the element hierarchy, it can only compute its result from whatever public accessors each element exposes. An operation that needs a field the element class never intended to expose publicly, purely internal layout state, for instance, forces that class to widen its public API just to satisfy a visitor's needs, coupling the element's encapsulation boundary to the requirements of external, independently-evolving operations.

Genuine cost This is widely considered Visitor's most understated drawback: the pattern optimizes for adding operations freely, but it does so partly by pushing state outward through the element's public surface, exactly the opposite of what a plain polymorphic method (which can freely access private fields from inside the class) would require.

55. How would you design an Element's public API to expose just enough state for visitors to operate on, without turning the class into a bare data holder with no encapsulation at all?

Prefer exposing a small number of intention-revealing accessors, or a single immutable snapshot/record of the state visitors actually need, rather than a getter per private field; this keeps the element's internal representation free to change independently of what visitors consume, as long as the snapshot's shape stays stable.

record ShapeSnapshot(String kind, double primaryDimension, double secondaryDimension) {}

interface Shape {
    void accept(Visitor v);
    ShapeSnapshot snapshot(); // one deliberate, stable read surface for visitors
}

This does not eliminate the trade-off from Q54, it just concentrates it into one deliberately designed method instead of letting it sprawl across many ad-hoc getters added reactively as each new visitor's needs arise.

56. Explain the "expression problem" from programming language theory and how it precisely characterizes the trade-off between Visitor and plain polymorphism discussed in Q9.

The expression problem asks whether a language or design lets you add both new data variants (element types) and new operations over existing data, to an existing system, without modifying existing code and without sacrificing static type safety. Plain polymorphism solves the "add new variants easily" half at the cost of the other; Visitor solves the "add new operations easily" half at the cost of the other; neither, in standard object-oriented Java, solves both simultaneously.

This is a genuinely open language-design problem, not a Java-specific limitation; languages with different feature sets (type classes in Haskell, multimethods in Common Lisp) approach it differently, but the Visitor-versus-polymorphism trade-off in Java is a direct, concrete instance of the expression problem in practice.

Expression problemData vs operations extensibility

57. Describe the Acyclic Visitor variant of the pattern and explain what dependency problem it solves compared to the classic Visitor design.

In the classic design, the Visitor interface must declare an overload for every element type up front, which means the interface (and therefore every class that implements it) depends on the entire element hierarchy, even if a given operation only cares about one or two element types. Acyclic Visitor breaks this by giving each element type its own tiny, separate visitor interface (CircleVisitor, SquareVisitor), so a ConcreteVisitor implements only the specific small interfaces relevant to the element types it actually handles, and an element's accept() safely casts the passed-in visitor to check whether it supports that element's specific interface.

interface Visitor {}                          // marker only, no methods
interface CircleVisitor extends Visitor { void visit(Circle c); }
interface SquareVisitor extends Visitor { void visit(Square s); }

class Circle implements Shape {
    public void accept(Visitor v) {
        if (v instanceof CircleVisitor cv) cv.visit(this); // supports partial visitors
    }
}
Acyclic VisitorBreaks the hierarchy-wide dependency

58. Explain how Acyclic Visitor's use of instanceof checks inside accept() reintroduces some of the runtime-check character the classic Visitor pattern was designed to avoid, and what safety it retains despite this.

Because each element's accept() now must check instanceof SpecificVisitorInterface before it can call the matching visit(), an operation that "forgets" to implement a given element's small visitor interface is simply skipped at runtime, no compile error at all, unlike the classic Visitor's hard compiler-enforced exhaustiveness (Q15). What Acyclic Visitor does retain is per-call type safety, no unchecked casts on the element's actual field data, and the freedom to add new element types without forcing every existing ConcreteVisitor to change, since each visitor now only opts into the element interfaces it cares about.

Net trade Acyclic Visitor trades classic Visitor's compile-time exhaustiveness for decoupling from the whole hierarchy; it is a deliberate choice for large, evolving hierarchies where that decoupling matters more than guaranteed completeness.

59. Design a hierarchical visitor for a Composite tree that distinguishes entering a composite node (pre-order) from leaving it (post-order), and explain why a single visit() call per node is not enough for this.

A single visit(FolderNode) call fired once per node cannot distinguish "about to descend into children" from "finished processing children," which matters for operations like computing indentation depth for pretty-printing, or closing a bracket after a nested block in code generation. The fix is to give composite nodes two visitor callbacks, invoked immediately before and immediately after the children loop.

interface HierarchicalVisitor {
    void visit(FileNode file);
    boolean enterFolder(FolderNode folder); // pre-order: return false to skip children
    void leaveFolder(FolderNode folder);    // post-order
}

class FolderNode implements FsNode {
    public void accept(HierarchicalVisitor v) {
        if (v.enterFolder(this)) {
            for (FsNode child : children) child.accept(v);
        }
        v.leaveFolder(this);
    }
}

60. Explain how a generic Visitor<R> can return a different concrete result type per element type while still satisfying one shared interface signature, using bounded wildcards if needed.

The interface itself commits to one return type R per implementation (Q11), but that R can be an umbrella type, a common supertype or a sealed interface of possible results, letting each visit() overload return a different concrete subtype while the caller still receives a single statically-typed value.

sealed interface RenderOutput permits VectorOutput, RasterOutput {}
record VectorOutput(String svgPath) implements RenderOutput {}
record RasterOutput(byte[] pixels) implements RenderOutput {}

class RenderVisitor implements Visitor<RenderOutput> {
    public RenderOutput visit(Circle c) { return new VectorOutput("M ..."); }
    public RenderOutput visit(Icon icon) { return new RasterOutput(icon.pixels()); }
}

61. Compare a void-returning, side-effecting Visitor interface against a generic Visitor<R> that returns a value, and explain the API design consequences of choosing one over the other up front.

A void Visitor is simpler to write for pure side-effecting operations, logging, rendering to a canvas, mutating external state, but forces any future need for a computed result to be smuggled in via a mutable field on the visitor itself, as AreaVisitor's total field does. A generic Visitor<R> makes the return value explicit and composable, useful for recursive computations where a parent node's result depends directly on a child's returned value (as in Q20's expression evaluator), at the cost of every implementation having to specify a type parameter even for operations that don't conceptually need one.

Since retrofitting a void interface into a generic one later breaks every existing ConcreteVisitor's method signatures, this choice is worth making deliberately up front, based on whether any planned operation is likely to need a genuinely composed return value.

62. Describe a CompositeVisitor that runs several independent visitors over the same traversal in a single pass, and the constraints this places on the visitors being combined.

A composite visitor implements the shared Visitor interface and simply forwards each call to a list of delegate visitors in turn, letting a single traversal of the object structure apply several unrelated operations (validation, logging, metrics) at once instead of walking the same structure multiple times.

class CompositeVisitor implements Visitor {
    private final List<Visitor> delegates;
    CompositeVisitor(List<Visitor> delegates) { this.delegates = delegates; }
    public void visit(Circle c) { delegates.forEach(v -> v.visit(c)); }
    public void visit(Square s) { delegates.forEach(v -> v.visit(s)); }
}

This only works cleanly when the combined visitors are independent of each other's side effects and order-insensitive; if one visitor's result depends on another's having already run, the composite must also guarantee a specific delegate ordering.

63. Explain how the Null Object pattern can be combined with Visitor to provide a safe, do-nothing default operation without resorting to the dangerous fallback overload pattern from Q13.

Rather than adding a generic visit(Object) fallback to the shared interface (which silently swallows unhandled element types, per Q13/Q40), a NoOpVisitor can be a full, explicit ConcreteVisitor implementing every overload as an empty body, used deliberately as a safe default value in places that need "some visitor" but have no real operation to perform yet, such as a test fixture or an uninitialized configuration slot.

class NoOpVisitor implements Visitor {
    public void visit(Circle c) {}
    public void visit(Square s) {}
    public void visit(Triangle t) {}
}
Visitor active = config.hasCustomVisitor() ? config.customVisitor() : new NoOpVisitor();

Unlike Q13's fallback, adding a new element type still forces NoOpVisitor itself to be updated with a new empty method, preserving the compiler's exhaustiveness guarantee rather than bypassing it.

64. Design a PrettyPrintVisitor that renders a source-code-like abstract syntax tree back into readable text, including how it handles indentation across nested nodes.

Reusing Q20's expression tree, a pretty-printing visitor recursively calls accept() on child expressions and wraps the resulting text with the operator's own syntax, tracking indentation as a mutable field that increases and decreases around nested blocks (the same balanced-state concern raised in Q45).

class PrettyPrintVisitor implements ExprVisitor<String> {
    public String visit(Literal literal) { return String.valueOf(literal.value); }
    public String visit(Add add) { return "(" + add.left.accept(this) + " + " + add.right.accept(this) + ")"; }
    public String visit(Multiply multiply) { return "(" + multiply.left.accept(this) + " * " + multiply.right.accept(this) + ")"; }
}
// (2.0 + (3.0 * 4.0))

65. Design a tree-rewriting Visitor that transforms an expression tree, such as one that performs constant folding, and returns a brand-new tree rather than mutating the original in place.

A rewriting visitor is a Visitor<Expr>: each visit() recursively transforms child expressions first, then decides whether to rebuild the node as-is or replace it with a simplified equivalent, such as collapsing an Add of two literals into a single new Literal node, leaving the original tree completely untouched for callers who still hold a reference to it.

class ConstantFoldingVisitor implements ExprVisitor<Expr> {
    public Expr visit(Literal literal) { return literal; }
    public Expr visit(Add add) {
        Expr left = add.left.accept(this), right = add.right.accept(this);
        if (left instanceof Literal l && right instanceof Literal r) return new Literal(l.value + r.value);
        return new Add(left, right); // not foldable yet, rebuild with simplified children
    }
}

66. Design a rules engine that evaluates different rule types, such as ThresholdRule, PatternRule, and CompositeRule, using a Visitor to keep the evaluation logic separate from the rule definitions.

Each rule type implements accept(RuleVisitor), and an EvaluationVisitor implements the actual pass/fail logic per rule kind, while a completely separate ExplanationVisitor can render a human-readable reason for why a rule passed or failed, both operating over the exact same rule definitions with no duplication of the rule tree structure itself.

interface RuleVisitor { boolean visit(ThresholdRule r); boolean visit(PatternRule r); boolean visit(CompositeRule r); }

class CompositeRule implements Rule {
    List<Rule> children; boolean requireAll;
    public boolean accept(RuleVisitor v) { return v.visit(this); }
}
class EvaluationVisitor implements RuleVisitor {
    public boolean visit(CompositeRule r) {
        return r.requireAll ? r.children.stream().allMatch(c -> c.accept(this))
                            : r.children.stream().anyMatch(c -> c.accept(this));
    }
}

67. Describe how a GUI component hierarchy (Button, Panel, TextField) can use a Visitor to implement cross-cutting operations like theming or accessibility auditing without adding those concerns directly to each widget class.

Each widget type implements accept(ComponentVisitor), and a ThemeVisitor applies a new color scheme's styles across every widget type in one pass, while a completely independent AccessibilityAuditVisitor walks the same component tree checking for missing labels or insufficient contrast, neither one requiring any widget class to know about theming or accessibility rules directly.

interface ComponentVisitor { void visit(Button b); void visit(Panel p); void visit(TextField t); }

class AccessibilityAuditVisitor implements ComponentVisitor {
    List<String> issues = new ArrayList<>();
    public void visit(Button b) { if (b.accessibleLabel() == null) issues.add("Button missing label: " + b.id()); }
    public void visit(Panel p) { p.children().forEach(c -> c.accept(this)); }
}

68. Explain how a database query planner's abstract syntax tree, with nodes for filters, joins, and projections, might use a Visitor to implement an optimization pass such as predicate pushdown.

A query plan is naturally tree-shaped (a Join node has two child plans, a Filter node wraps one), making it a good fit for the same rewriting-visitor style from Q65: a PredicatePushdownVisitor recursively rewrites the tree, moving a Filter node below a Join where semantically valid, and returns a new, optimized plan tree while leaving the original untouched.

interface PlanVisitor { PlanNode visit(FilterNode f); PlanNode visit(JoinNode j); PlanNode visit(ScanNode s); }

class PredicatePushdownVisitor implements PlanVisitor {
    public PlanNode visit(FilterNode f) {
        if (f.child() instanceof JoinNode join && canPushBelow(f.predicate(), join.left())) {
            return new JoinNode(new FilterNode(f.predicate(), join.left()), join.right());
        }
        return new FilterNode(f.predicate(), f.child().accept(this));
    }
}

Separate optimization passes, cost estimation, join reordering, are each their own ConcreteVisitor over the same plan-node hierarchy, mirroring the compiler-pass structure from Q22.

69. Describe using a Visitor to process a generic JSON or XML DOM tree (object, array, string, number nodes) uniformly, such as computing a checksum over the whole document.

A JSON DOM's node kinds (object, array, string, number, boolean, null) form a small, very stable set, an ideal fit for Visitor, since new JSON node kinds essentially never appear while operations over a JSON tree, pretty-printing, schema validation, redaction of sensitive fields, checksumming, keep growing.

interface JsonVisitor { void visit(JsonObject o); void visit(JsonArray a); void visit(JsonString s); void visit(JsonNumber n); }

class ChecksumVisitor implements JsonVisitor {
    private final MessageDigest digest = MessageDigest.getInstance("SHA-256");
    public void visit(JsonObject o) { o.entries().forEach((k, v) -> { digest.update(k.getBytes()); v.accept(this); }); }
    public void visit(JsonString s) { digest.update(s.value().getBytes(StandardCharsets.UTF_8)); }
}

70. Design a ValidationVisitor for a shopping cart containing different line-item types, such as PhysicalItem, DigitalItem, and SubscriptionItem, each with different validation rules.

Each cart item type implements accept(CartItemVisitor), and ValidationVisitor applies type-specific checks, requiring a shipping address for PhysicalItem, requiring an email for DigitalItem delivery, requiring a payment method that supports recurring billing for SubscriptionItem, accumulating any violations into a shared list.

class ValidationVisitor implements CartItemVisitor {
    private final List<String> errors = new ArrayList<>();
    private final Order order;
    public void visit(PhysicalItem item) { if (order.shippingAddress() == null) errors.add("Missing shipping address for " + item.sku()); }
    public void visit(DigitalItem item) { if (order.deliveryEmail() == null) errors.add("Missing delivery email for " + item.sku()); }
    public void visit(SubscriptionItem item) { if (!order.paymentMethod().supportsRecurring()) errors.add("Payment method can't support subscription " + item.sku()); }
}

71. Design a ShippingCostVisitor that calculates cost differently across the same cart item types from Q70, and confirm no changes are needed to PhysicalItem, DigitalItem, or SubscriptionItem.

This is exactly Q5's payoff applied to a real domain: ShippingCostVisitor is a second, entirely independent ConcreteVisitor implementing CartItemVisitor, and adding it required zero edits to the three item classes already validated in Q70.

class ShippingCostVisitor implements CartItemVisitor {
    double total;
    public void visit(PhysicalItem item) { total += item.weightKg() * 2.50; }
    public void visit(DigitalItem item) { /* no shipping cost */ }
    public void visit(SubscriptionItem item) { /* no shipping cost, recurring billing only */ }
}

72. Design a TaxVisitor that computes tax across an invoice's mixed line-item types, where different item categories (goods, services, digital products) have different tax treatments by jurisdiction.

Since tax rules vary by both item category and jurisdiction, the visitor's constructor takes the applicable jurisdiction's tax rules, and each visit() overload applies the category-specific rate from those rules, so swapping in a different jurisdiction's TaxVisitor instance requires no change to any invoice line-item class.

class TaxVisitor implements LineItemVisitor {
    private final TaxRules rules;
    double totalTax;
    TaxVisitor(TaxRules rules) { this.rules = rules; }
    public void visit(GoodsLineItem item) { totalTax += item.amount() * rules.goodsRate(); }
    public void visit(ServiceLineItem item) { totalTax += item.amount() * rules.serviceRate(); }
    public void visit(DigitalProductLineItem item) { totalTax += item.amount() * rules.digitalRate(); }
}

73. Design a multi-format export system where the same underlying report tree can be exported to CSV, JSON, and XML, each as its own ConcreteVisitor, and explain how a new format is added later.

Extending Q24's export pattern, three ConcreteVisitors, CsvExportVisitor, JsonExportVisitor, XmlExportVisitor, each implement the same ReportVisitor interface over the same report-node hierarchy. Adding a fourth format, YAML, is one new class implementing ReportVisitor; the report's node classes, Section, Table, Chart, are never touched.

interface ReportVisitor { void visit(Section s); void visit(Table t); void visit(Chart c); }
class YamlExportVisitor implements ReportVisitor { /* new format, zero changes elsewhere */ }
One interface, many export formats

74. Design an access-control Visitor that checks whether the current user can perform an operation across a resource hierarchy with different resource types (Document, Folder, SharedLink).

An AccessCheckVisitor, constructed with the current user's identity and requested permission level, implements type-specific authorization rules, a Folder might inherit permissions from its parent, while a SharedLink checks an expiration timestamp in addition to the requesting user's role.

class AccessCheckVisitor implements ResourceVisitor {
    private final User user; private final Permission requested;
    boolean granted;
    public void visit(Document doc) { granted = doc.acl().allows(user, requested); }
    public void visit(SharedLink link) { granted = !link.isExpired() && link.allows(requested); }
    public void visit(Folder folder) { granted = folder.acl().allows(user, requested) || folder.parent().map(p -> { p.accept(this); return granted; }).orElse(false); }
}

75. Explain how to register multiple ConcreteVisitor implementations as Spring beans and have the correct one selected at runtime based on a request parameter or feature flag.

Declare each ConcreteVisitor as a Spring-managed @Component implementing the shared interface, inject them collectively as a List<ReportVisitor> or a Map<String, ReportVisitor> keyed by bean name, and select the right one at request time by matching a format parameter against the bean name or an explicit qualifier annotation.

@Component("csv") class CsvExportVisitor implements ReportVisitor { /* ... */ }
@Component("json") class JsonExportVisitor implements ReportVisitor { /* ... */ }

@RestController
class ExportController {
    private final Map<String, ReportVisitor> visitorsByFormat; // Spring injects by bean name
    ExportController(Map<String, ReportVisitor> visitorsByFormat) { this.visitorsByFormat = visitorsByFormat; }

    @GetMapping("/export")
    void export(@RequestParam String format) {
        ReportVisitor visitor = visitorsByFormat.get(format);
        report.accept(visitor);
    }
}

76. Design an audit-logging Visitor that records which command-hierarchy nodes were executed, without adding logging code directly into every Command subclass.

An AuditVisitor implements CommandVisitor and, for each command type it encounters, writes a structured log entry containing the command's type, its key parameters, and a timestamp, keeping every logging concern in one place instead of scattered across each command's own execute() method.

class AuditVisitor implements CommandVisitor {
    private final AuditLog log;
    public void visit(TransferFundsCommand cmd) {
        log.record("TRANSFER", Map.of("from", cmd.fromAccount(), "to", cmd.toAccount(), "amount", cmd.amount()));
    }
    public void visit(CloseAccountCommand cmd) {
        log.record("CLOSE_ACCOUNT", Map.of("account", cmd.accountId()));
    }
}

77. Describe combining the Visitor pattern with the Observer pattern, where a Visitor's traversal publishes events that observers react to, decoupling the traversal from what happens with its results.

A NotifyingVisitor implements the visitor interface but, instead of accumulating a result itself, publishes an event to a set of registered observers each time it processes an element, letting completely unrelated subsystems (a metrics collector, a UI progress bar, an audit log) react to the same traversal without the visitor needing to know who's listening.

class NotifyingVisitor implements FsVisitor {
    private final List<Consumer<FsNode>> observers = new ArrayList<>();
    void subscribe(Consumer<FsNode> observer) { observers.add(observer); }
    public void visit(FileNode file) { observers.forEach(o -> o.accept(file)); }
}

78. Discuss the performance overhead of the Visitor pattern's double dispatch in a performance-critical traversal, and how to measure it correctly using a proper benchmarking tool.

Each accept()/visit() pair is two virtual method calls, both of which a JIT compiler will typically inline once a call site is confirmed monomorphic (always hitting the same concrete types) during warm-up; measured cold or under megamorphic conditions (many different element types hitting the same call site unpredictably), the overhead is more visible since the JIT falls back to a slower dispatch mechanism.

@Benchmark
public double visitorDispatch(Blackhole bh) {
    AreaVisitor v = new AreaVisitor();
    for (Shape s : shapes) s.accept(v);
    return v.total;
}
Measure, don't guess Use JMH with adequate warm-up iterations rather than manual System.nanoTime() timing; naive timing loops are routinely skewed by JIT warm-up and dead-code elimination.

79. Discuss the memory implications of a stateful Visitor that accumulates results across a very large traversal, such as a report over millions of database rows modeled as elements.

An accumulator field that grows unboundedly, a List collecting one entry per visited element, can exhaust heap on a sufficiently large traversal even though each individual visit() call is cheap; the fix is usually to make the visitor stream its output incrementally (writing to a file or a bounded buffer as it goes) rather than holding the full accumulated result in memory until the traversal completes.

class StreamingExportVisitor implements RowVisitor, AutoCloseable {
    private final BufferedWriter writer;
    public void visit(Row row) { writer.write(row.toCsvLine()); } // written immediately, not buffered in a List
    public void close() throws IOException { writer.close(); }
}

80. Discuss debugging tips for a Visitor-based pipeline where stack traces grow deep due to recursive accept()/visit() calls across a nested tree, making it harder to pinpoint where a problem originated.

A stack trace through a recursive tree traversal alternates accept() and visit() frames dozens of levels deep, which obscures the logical "current node" more than a flat loop would. Attaching contextual information, such as the current node's path or identifier, to any exception thrown mid-traversal (rather than letting a bare NullPointerException propagate unadorned) makes the failure immediately locatable without having to read every stack frame.

public void visit(FileNode file) {
    try {
        process(file);
    } catch (RuntimeException ex) {
        throw new VisitorException("Failed while visiting " + file.path(), ex);
    }
}

81. Discuss how to document a Visitor-based API for other engineers on the team, especially communicating the Visitor's dilemma from Q8 so future contributors understand the cost of adding a new element type.

Beyond ordinary Javadoc on the Visitor interface and each element class, it's worth explicitly documenting, in a comment on the Visitor interface itself, that it is intentionally exhaustive and that adding a new element type is expected to require updating every implementation, listing the currently-known implementations so a future engineer can find and update them all rather than discovering the compile errors one at a time.

/**
 * Exhaustive by design: adding a new Shape subtype requires adding a matching
 * visit() overload here AND updating every existing implementation, currently:
 * AreaVisitor, RenderVisitor, PerimeterVisitor. See Q8 for why this is intentional.
 */
interface Visitor { void visit(Circle c); void visit(Square s); }

82. Walk through refactoring a legacy codebase that dispatches behavior with a long instanceof/pattern-matching if-else chain into a proper Visitor pattern, step by step.

First, identify every distinct branch in the chain to determine the full set of element types; second, introduce an accept(Visitor) method on each of those classes (retrofitting existing classes if needed); third, move each branch's logic body into the matching visit() overload of a new ConcreteVisitor class; fourth, replace every call site of the old chain with a single element.accept(visitor) call.

// before
if (shape instanceof Circle c) { area = Math.PI * c.radius * c.radius; }
else if (shape instanceof Square s) { area = s.side * s.side; }
else if (shape instanceof Triangle t) { area = 0.5 * t.base * t.height; }

// after
double area = shape.accept(new AreaVisitor());

This refactor is worth it specifically when the same instanceof chain is duplicated in more than one place in the codebase; a single isolated chain used only once may not justify the added ceremony (see Q89 for the direct comparison).

83. Describe how an annotation processor could auto-generate the repetitive accept() method for every Element subclass, removing that boilerplate from hand-written source while keeping double dispatch intact.

A processor scans for classes annotated @VisitableElement and, at compile time, generates a companion class or an interface default method supplying the accept(Visitor v) { v.visit(this); } body for each one, using the annotated class's own name to guarantee this carries the correct compile-time type inside the generated code (avoiding the Q14 pitfall since the generated method still lives inside the specific subclass's own compilation unit).

@VisitableElement
class Circle implements Shape { double radius; } // accept() generated by the annotation processor

This removes the manual repetition from Q6 without weakening the double-dispatch guarantee, at the cost of an extra build-time dependency and a slightly less discoverable accept() method (it won't appear directly in the hand-written source file).

84. Explain how a Visitor can assemble an immutable final result across a traversal, rather than mutating a shared mutable accumulator field, and why this can be preferable for correctness.

Instead of a mutable field mutated in place across every visit() call (with the thread-safety risks from Q38), a visitor can build up an immutable, append-only structure, for example threading an immutable list through each call and reassigning it, or, more idiomatically in modern Java, using the generic-return style from Q11 so each recursive call simply returns a new immutable value rather than mutating shared state at all.

class ImmutableCollectingVisitor implements Visitor<List<String>> {
    public List<String> visit(Circle c) { return List.of("circle:" + c.radius); }
    public List<String> visit(Square s) { return List.of("square:" + s.side); }
}
List<String> all = shapes.stream().flatMap(s -> s.accept(visitor).stream()).toList();

85. Describe a plugin architecture where each plugin contributes a new operation over a shared, stable Element hierarchy by supplying its own ConcreteVisitor, without needing access to modify the core codebase.

The core module ships the Element hierarchy and the Visitor interface as a stable public API; each plugin, packaged and deployed independently, implements Visitor with its own operation and registers itself through a plugin-discovery mechanism such as ServiceLoader, letting the host application discover and apply every installed plugin's visitor without the core module ever being recompiled.

// core module ships this stable interface
public interface ShapeOperationPlugin extends Visitor {}

// a plugin jar contributes:
public class WatermarkPlugin implements ShapeOperationPlugin { /* new operation, no core changes */ }
// META-INF/services/com.example.ShapeOperationPlugin lists WatermarkPlugin

ServiceLoader.load(ShapeOperationPlugin.class).forEach(plugin -> shape.accept(plugin));

86. Explain how to use a bounded generic, such as <T extends Element>, in a Visitor-related utility method that must accept any element subtype while still returning a strongly-typed result for that specific subtype.

A bounded type parameter lets a generic helper method operate on "any element type" while preserving the caller's specific static type through the method's return value, useful for a utility that wraps accept() with cross-cutting behavior, such as timing, without erasing which concrete element was passed in.

static <T extends Shape, R> R timedAccept(T element, Visitor<R> visitor) {
    long start = System.nanoTime();
    try {
        return element.accept(visitor);
    } finally {
        System.out.println(element.getClass().getSimpleName() + " took " + (System.nanoTime() - start) + "ns");
    }
}

87. Discuss splitting one large Visitor interface with a dozen overloaded methods into several smaller, more focused Visitor interfaces grouped by related element categories.

When a single Visitor interface grows to cover many unrelated element categories, shapes, documents, network packets, all in one, every ConcreteVisitor is forced to implement overloads for categories it doesn't care about (unless a Q13-style fallback is used, which is discouraged). Splitting into ShapeVisitor, DocumentVisitor, and PacketVisitor, each covering only its own related element family, keeps each ConcreteVisitor focused and lets operations that only care about shapes avoid depending on document or packet types at all.

interface ShapeVisitor { void visit(Circle c); void visit(Square s); }
interface DocumentVisitor { void visit(Paragraph p); void visit(Table t); }
// a ConcreteVisitor implements only the interface(s) relevant to its actual job

88. Explain how default methods on a Visitor interface, providing an optional no-op body for infrequently-used element types, mirror the AWT WindowAdapter idiom, and what exhaustiveness guarantee this quietly gives up.

Java's WindowAdapter gave every method of the multi-method WindowListener interface an empty no-op default, letting subclasses override only the one event they cared about. A Visitor interface can do the same for element types most ConcreteVisitors don't need to handle meaningfully, defining a default empty body for, say, a rarely-relevant Watermark element type, so most ConcreteVisitors don't need to write an empty override for it.

interface Visitor {
    void visit(Circle c);
    void visit(Square s);
    default void visit(Watermark w) {} // convenience default, most visitors ignore watermarks
}

The trade-off is precisely the one from Q13: any ConcreteVisitor that actually should react to Watermark but simply forgets to override it will compile silently and quietly do nothing, since the compiler can no longer force that override the way it forces a plain abstract method.

89. Directly compare the classic Visitor pattern against a simple instanceof (or switch) chain for a small, genuinely stable element hierarchy, and state plainly when the switch is simply the better engineering choice.

For a hierarchy of two or three types that is realistically never going to grow, and with only one or two operations that will ever be needed, a plain switch (ideally pattern-matching over a sealed type, per Q26, for compile-time exhaustiveness without any of the Visitor ceremony) is simply less code, easier for a new team member to read top to bottom, and avoids the encapsulation cost from Q54 entirely.

// perfectly reasonable for a small, stable, single-operation case:
double area = switch (shape) {
    case Circle c -> Math.PI * c.radius() * c.radius();
    case Square s -> s.side() * s.side();
};

Reach for full Visitor specifically once you have (or expect) multiple independent operations, external plugin contributors, or a hierarchy you cannot seal, none of which the small switch example above needs to worry about.

90. Discuss strategies for handling and aggregating exceptions thrown partway through a multi-element traversal, where one element's visit() call fails but the rest of the traversal should still complete.

Rather than letting the first exception abort the entire traversal, the visitor (or the object structure driving it) can catch each element's exception individually, record it alongside which element failed, and continue to the next element, surfacing every collected failure together at the end instead of stopping at the first one.

class ResilientVisitor implements Visitor {
    private final List<VisitFailure> failures = new ArrayList<>();
    public void visit(Circle c) {
        try { process(c); } catch (RuntimeException ex) { failures.add(new VisitFailure(c, ex)); }
    }
    List<VisitFailure> failures() { return failures; } // caller decides how to react
}
Partial failureCollect, don't abort

91. Explain how to support both an old and a new version of an Element type simultaneously during an API deprecation window, without breaking existing ConcreteVisitors written against the old shape.

Add the new element type's visit() overload to the interface as a default method that adapts the new type into the old one internally (so legacy ConcreteVisitors keep compiling untouched, effectively getting a translated call to the old overload), while newly-written ConcreteVisitors can choose to override the new overload directly for full fidelity once the old type is eventually retired.

interface Visitor {
    void visit(CircleV1 c); // legacy
    default void visit(CircleV2 c) { visit(c.toLegacyV1()); } // adapts new -> old by default
}
API versioningDeprecation window

92. Discuss how generics and type erasure complicate overload resolution when an Element type itself is generic, such as Box<T>, and a Visitor needs to visit differently depending on T.

Because Java erases generic type parameters at compile time, Box<String> and Box<Integer> share exactly one runtime class, Box, so a Visitor cannot declare separate overloads visit(Box<String>) and visit(Box<Integer>), both erase to the identical signature visit(Box) and the compiler rejects the duplicate. The visitor must instead accept the erased Box<?> and branch internally on the contained value's runtime type, or the element itself must expose a type tag the visitor can switch on.

public void visit(Box<?> box) {
    Object value = box.get();
    if (value instanceof String s) { /* ... */ }
    else if (value instanceof Integer i) { /* ... */ }
}

93. Describe how a static-analysis tool, such as a linter checking for a forbidden API call, could use a Visitor over a parsed source-code AST to implement one specific rule as one ConcreteVisitor.

Building directly on Q19's compiler tree API, a linter rule is naturally expressed as one ConcreteVisitor: it overrides only the node kinds relevant to its check, for example visitMethodInvocation, and records a diagnostic whenever the invoked method matches a forbidden signature, while dozens of unrelated rules coexist as separate ConcreteVisitor classes over the exact same AST, each independently addable without touching the others.

class ForbiddenApiRule extends SimpleTreeVisitor<Void, Void> {
    public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
        if (isForbidden(node)) reportDiagnostic(node, "Call to forbidden API");
        return super.visitMethodInvocation(node, unused);
    }
}

94. Design a spreadsheet application's cell-formula evaluation using a Visitor over different cell types, such as NumberCell, FormulaCell, and TextCell, and describe how a FormulaCell recursively evaluates its dependencies.

An EvaluationVisitor implements visit() per cell type: NumberCell simply returns its stored value, TextCell returns not-a-number or zero depending on the spreadsheet's convention, and FormulaCell recursively calls accept() on each cell reference in its formula before combining the results, exactly the recursive accept()-calling-accept() pattern from Q20's expression evaluator.

class EvaluationVisitor implements CellVisitor<Double> {
    private final Sheet sheet;
    public Double visit(NumberCell c) { return c.value(); }
    public Double visit(TextCell c) { return 0.0; }
    public Double visit(FormulaCell c) {
        return c.references().stream().mapToDouble(ref -> sheet.cellAt(ref).accept(this)).sum();
    }
}

95. Explain how a compiler's semantic analysis (type-checking) phase can be implemented as its own Visitor pass, entirely separate from the parsing visitor and the code-generation visitor discussed elsewhere in this guide.

A compiler pipeline over the same AST from Q19 and Q22 typically runs several distinct visitor passes in sequence: a parsing pass builds the tree in the first place, a TypeCheckVisitor then walks it purely to verify type correctness and annotate each expression node with its inferred type, and a later CodeGenVisitor walks the same, now-annotated tree to emit bytecode or machine code, each pass a wholly separate ConcreteVisitor with no knowledge of the others' internals.

class TypeCheckVisitor implements ExprVisitor<Type> {
    public Type visit(Add add) {
        Type leftType = add.left().accept(this), rightType = add.right().accept(this);
        if (!leftType.isNumeric() || !rightType.isNumeric()) throw new TypeError("+ requires numeric operands");
        return Type.NUMBER;
    }
}

96. Discuss how a large tree traversal could be parallelized safely across multiple threads using the Visitor pattern, and what constraints the visitor's own state places on doing this correctly.

Parallelizing a Visitor-based traversal is straightforward when the visitor is stateless (Q39) and each subtree can be processed independently, for example splitting a wide composite node's children across a ForkJoinPool and combining each child's independently-returned result; it becomes unsafe the moment the visitor accumulates into a single shared mutable field across threads, exactly the race condition from Q38.

class ParallelAreaTask extends RecursiveTask<Double> {
    private final Shape shape;
    protected Double compute() {
        if (shape instanceof CompositeShape composite) {
            return composite.children().stream()
                .map(child -> new ParallelAreaTask(child).fork())
                .mapToDouble(ForkJoinTask::join).sum();
        }
        return shape.accept(new AreaVisitor()); // fresh, unshared visitor instance per leaf
    }
}

97. Summarize the most common Visitor pattern anti-patterns seen in real codebases, beyond the specific bugs already covered, and how to recognize each one during code review.

Common anti-patterns include: using Visitor for a hierarchy with exactly one operation and no realistic prospect of a second one (unnecessary ceremony, see Q10/Q89); a Visitor interface that has quietly grown to cover several unrelated element families at once (see Q87's fix); a ConcreteVisitor that mutates the elements it visits when nothing about its name suggests it should (see Q12); and a Visitor interface polluted with a fallback overload purely to make one lazy ConcreteVisitor compile (see Q13).

Review checklist Ask: is there a genuine second operation planned, does the interface stay focused on one element family, is any mutation clearly signposted in the visitor's name, and does every implementation handle every overload explicitly with no silent fallback.

98. In an interview setting, how would you concisely explain the Visitor pattern's core trade-off in under a minute, without reciting a rote textbook definition?

A strong concise answer: "Visitor lets me add a new operation over a fixed set of classes without touching those classes, by having each class call back into the operation with itself, so the right method gets picked based on both the class's and the operation's actual type at runtime. The catch is it only works well if that set of classes is stable, because adding a new one means updating every operation I've ever written. If my classes change a lot and my operations don't, I'd use polymorphism instead; on a modern JDK with a closed hierarchy, I might reach for sealed types and pattern-matching switch instead of writing the Visitor boilerplate at all."

This signals, in a few sentences, that the candidate understands the mechanism (double dispatch), the trade-off (Q9), and current alternatives (Q26), which is exactly what distinguishes a candidate reasoning from understanding versus one reciting a memorized definition (echoing the judgment signal from the Prototype guide's own capstone question).

99. Design a hybrid approach for Java 21 that fully combines sealed element types with a classic accept()-based Visitor, and explain the specific scenario that justifies keeping both mechanisms rather than choosing just one.

Building on Q34's hybrid idea with a concrete worked example: seal the element hierarchy so internal code gets compiler-verified exhaustive switches for quick, one-off queries, but keep accept(Visitor) on the interface for operations that are substantial enough to warrant their own class, need constructor-injected dependencies, or must be contributed by an external module via the plugin mechanism from Q85.

sealed interface Shape permits Circle, Square {
    void accept(Visitor visitor); // for substantial, stateful, or externally-contributed operations
}
record Circle(double radius) implements Shape {
    public void accept(Visitor v) { v.visit(this); }
}

// quick internal query: plain exhaustive switch, no Visitor needed
boolean isLarge = switch (shape) { case Circle c -> c.radius() > 100; case Square s -> s.side() > 100; };

// substantial, stateful operation: full Visitor
shape.accept(new RenderVisitor(canvas, theme));

The deciding factor, stated plainly: use the switch for small, local, single-pass logic; reach for the Visitor when the operation is big enough, stateful enough, or externally contributed enough to deserve being its own named, independently testable class.

100. Capstone: design a complete Visitor-based system for a Word-like document processing pipeline with Paragraph, Table, and Image elements, supporting a SpellCheckVisitor, an ExportVisitor, and a WordCountVisitor added over time.

Start with the stable element hierarchy and a focused, single-purpose DocVisitor interface (Q87's lesson: keep it scoped to document elements only). Each element implements accept() individually (Q6/Q14), with Table recursing into its rows the way Q23's composite handling requires.

interface DocVisitor { void visit(Paragraph p); void visit(Table t); void visit(Image img); }
interface DocElement { void accept(DocVisitor v); }

class Paragraph implements DocElement {
    String text;
    public void accept(DocVisitor v) { v.visit(this); }
}
class Table implements DocElement {
    List<Row> rows;
    public void accept(DocVisitor v) { v.visit(this); rows.forEach(r -> r.accept(v)); }
}

The first shipped operation, WordCountVisitor, is one class accumulating a count across paragraph text and table cell text (Q39's stateful-accumulator style, single-use per document). Months later, SpellCheckVisitor and ExportVisitor (to the multiple formats from Q73) are added as entirely new classes, with zero changes to Paragraph, Table, or Image, exactly the payoff promised back in Q5, now demonstrated end to end across a realistic, evolving document pipeline.

CapstoneStable elements, growing operationsZero element-class churn
No comments
Leave a Comment