Represent an operation to be performed on elements of an object structure — define a new operation without changing the classes of the elements it operates on.
Visitor lets you add new operations to an existing object structure without modifying the classes of those objects. You separate the algorithm from the object structure — the algorithm lives in a Visitor class, while the object's accept(visitor) method dispatches the right visitor method via double dispatch.
Every time you need a new operation on the structure, you add a new Visitor class rather than touching the existing element classes.
You have a document object model: Paragraph, Image, Table elements. You need operations: export to HTML, export to PDF, spell-check. Adding each operation as a method to every element class bloats them all. Adding a fourth operation requires modifying all three element classes again.
Visitor puts each operation in its own class: HTMLExportVisitor, PDFExportVisitor, SpellCheckVisitor. Adding a new operation = one new class, zero changes to elements.
visit(ConcreteElement) overload for each concrete element type in the structure. One method per element class.accept(Visitor). This is the key — it lets the visitor know which concrete element it's dealing with via double dispatch.accept(v) as v.visit(this) — passing itself to the visitor. This is the double dispatch mechanism.accept(visitor) on each.// Visitor interface — one visit() overload per element type public interface DocumentVisitor { void visit(Paragraph paragraph); void visit(Image image); void visit(Table table); } // Element interface — accept dispatches to correct visit() via double dispatch public interface DocumentElement { void accept(DocumentVisitor visitor); } // Concrete Elements — each calls v.visit(this) public class Paragraph implements DocumentElement { private final String text; public Paragraph(String text) { this.text = text; } public String getText() { return text; } public void accept(DocumentVisitor v) { v.visit(this); } // double dispatch } public class Image implements DocumentElement { private final String src; private final int width, height; public Image(String src, int w, int h) { this.src=src; width=w; height=h; } public String getSrc() { return src; } public int getWidth() { return width; } public int getHeight() { return height; } public void accept(DocumentVisitor v) { v.visit(this); } } public class Table implements DocumentElement { private final List<List<String>> rows; public Table(List<List<String>> rows) { this.rows = rows; } public List<List<String>> getRows() { return rows; } public void accept(DocumentVisitor v) { v.visit(this); } } // ConcreteVisitor 1 — HTML export operation public class HTMLExportVisitor implements DocumentVisitor { private final StringBuilder html = new StringBuilder(); public void visit(Paragraph p) { html.append("<p>").append(p.getText()).append("</p>\n"); } public void visit(Image img) { html.append("<img src='").append(img.getSrc()) .append("' width='").append(img.getWidth()).append("'/>\n"); } public void visit(Table t) { html.append("<table>\n"); t.getRows().forEach(row -> { html.append(" <tr>"); row.forEach(cell -> html.append("<td>").append(cell).append("</td>")); html.append("</tr>\n"); }); html.append("</table>\n"); } public String getHTML() { return html.toString(); } } // ConcreteVisitor 2 — Spell check (separate operation, zero changes to elements) public class SpellCheckVisitor implements DocumentVisitor { private final List<String> errors = new ArrayList<>(); public void visit(Paragraph p) { if (p.getText().contains("teh")) errors.add("Typo in paragraph: teh"); } public void visit(Image img) { /* images have no text to check */ } public void visit(Table t) { t.getRows().stream().flatMap(List::stream) .filter(cell -> cell.contains("teh")) .forEach(c -> errors.add("Typo in table cell: " + c)); } public List<String> getErrors() { return errors; } } // Object Structure — holds elements, provides traversal public class Document { private final List<DocumentElement> elements = new ArrayList<>(); public void add(DocumentElement e) { elements.add(e); } public void accept(DocumentVisitor visitor) { elements.forEach(e -> e.accept(visitor)); // double dispatch here } } // Client public class Main { public static void main(String[] args) { Document doc = new Document(); doc.add(new Paragraph("Hello teh world")); doc.add(new Image("logo.png", 200, 100)); doc.add(new Table(List.of(List.of("Name", "Age"), List.of("Alice", "30")))); HTMLExportVisitor htmlVisitor = new HTMLExportVisitor(); doc.accept(htmlVisitor); System.out.println(htmlVisitor.getHTML()); SpellCheckVisitor spellVisitor = new SpellCheckVisitor(); doc.accept(spellVisitor); System.out.println("Errors: " + spellVisitor.getErrors()); } }
PointcutVisitor — walks pointcut expression treesjavax.lang.model.element.ElementVisitor — annotation processing uses Visitor patternVideo element is added to the structure — existing Visitors have no visit(Video) method. Java compile error if interface has it, but default no-op implementations silently ignore the new elementaccept(v) as v.visit(this) but mistakenly calls the wrong overload — visitor executes logic for the wrong element typevisitor.visit(anotherElement) instead of v.visit(this) — double dispatch breaks and the wrong element's logic is invokedsealed — every permitted subtype must be listed. Adding a new element type forces a compile error in all visitors that use exhaustive pattern matching, making gaps impossible to missdocument.accept() callsaccept(v) always calls v.visit(this) and nothing else. Enforce via code review or a lint rule. Any deviation breaks double dispatch silently.// ❌ BREAKING — new element not handled (default no-op) public interface DocumentVisitor { void visit(Paragraph p); void visit(Image img); default void visit(Video v) {} // ❌ default no-op — Video silently ignored } // ✅ FIX — all visit methods abstract, compiler forces implementation public interface DocumentVisitor { void visit(Paragraph p); void visit(Image img); void visit(Video v); // ✅ must implement — won't compile otherwise } // ✅ Java 17 sealed classes — exhaustive coverage enforced public sealed interface DocumentElement permits Paragraph, Image, Table, Video { void accept(DocumentVisitor v); } // Pattern match in visitor — compiler error if new permit type not handled public void process(DocumentElement e) { switch (e) { case Paragraph p -> visit(p); case Image i -> visit(i); case Table t -> visit(t); case Video v -> visit(v); // ✅ sealed — compiler ensures complete coverage } } // ❌ BREAKING — wrong double dispatch public class Image implements DocumentElement { public void accept(DocumentVisitor v) { v.visit(new Paragraph("")); // ❌ wrong type! visits Paragraph logic for an Image } } // ✅ FIX — accept() always and only calls v.visit(this) public class Image implements DocumentElement { public void accept(DocumentVisitor v) { v.visit(this); // ✅ always this — double dispatch is sacred } }
element.accept(v) → v.visit(this) — visitor always knows the exact concrete type.accept() call selects the right element class. Second dispatch = overloaded visit(ConcreteElement) selects the right visitor method. Together they give type-safe dispatch without instanceof.