Visitor Design Pattern in Java: Complete Guide with Examples

Your codebase has a shape hierarchy: Circle, Rectangle, Triangle. Each is stable — you rarely add new shapes. But you keep adding operations: calculate area, calculate perimeter, export to SVG, check collision, generate bounding box. Each new operation could be added as a method on every shape class. But that means touching three files every time you add an operation, and it couples rendering logic into the geometry classes.

Visitor inverts the structure: each operation is its own class. The shape hierarchy stays closed to modification; you add operations by adding new visitor classes. This is the open/closed principle applied to a class hierarchy — closed to modification, open to new operations.

Pattern Structure

Visitor design pattern structure (via refactoring.guru)
Visitor declares visit() for each element type. ConcreteVisitors implement each operation. Elements expose accept(Visitor). Double dispatch: the element calls visitor.visit(this) inside accept(). Diagram: refactoring.guru

Implementation: Shape Geometry Operations

package visitor;
/** Visitor interface — one visit() overload per concrete element type */
public interface ShapeVisitor {
    double visit(Circle circle);
    double visit(Rectangle rectangle);
    double visit(Triangle triangle);
}
package visitor;
/** Element interface — all shapes must accept a visitor */
public interface Shape {
    String name();
    /** accept() calls visitor.visit(this) — this is the double-dispatch trick */
    double accept(ShapeVisitor visitor);
}
package visitor;
public class Circle implements Shape {
    private final double radius;
    public Circle(double radius) { this.radius = radius; }
    public double getRadius() { return radius; }
    @Override public String name() { return "Circle(r=" + radius + ")"; }
    // Double dispatch: the runtime type of THIS (Circle) selects visit(Circle)
    @Override public double accept(ShapeVisitor visitor) { return visitor.visit(this); }
}
public class Rectangle implements Shape {
    private final double width, height;
    public Rectangle(double width, double height) { this.width = width; this.height = height; }
    public double getWidth() { return width; }
    public double getHeight() { return height; }
    @Override public String name() { return "Rectangle(" + width + "x" + height + ")"; }
    @Override public double accept(ShapeVisitor visitor) { return visitor.visit(this); }
}
public class Triangle implements Shape {
    private final double a, b, c;  // three side lengths
    public Triangle(double a, double b, double c) { this.a = a; this.b = b; this.c = c; }
    public double getA() { return a; } public double getB() { return b; } public double getC() { return c; }
    @Override public String name() { return "Triangle(" + a + "," + b + "," + c + ")"; }
    @Override public double accept(ShapeVisitor visitor) { return visitor.visit(this); }
}
package visitor;
/** ConcreteVisitor 1 — calculates area for each shape type */
public class AreaCalculator implements ShapeVisitor {
    @Override
    public double visit(Circle circle) {
        return Math.PI * circle.getRadius() * circle.getRadius();
    }
    @Override
    public double visit(Rectangle rectangle) {
        return rectangle.getWidth() * rectangle.getHeight();
    }
    @Override
    public double visit(Triangle triangle) {
        // Heron's formula
        double s = (triangle.getA() + triangle.getB() + triangle.getC()) / 2;
        return Math.sqrt(s * (s - triangle.getA()) * (s - triangle.getB()) * (s - triangle.getC()));
    }
}
/** ConcreteVisitor 2 — calculates perimeter for each shape type */
public class PerimeterCalculator implements ShapeVisitor {
    @Override
    public double visit(Circle circle) {
        return 2 * Math.PI * circle.getRadius();
    }
    @Override
    public double visit(Rectangle rectangle) {
        return 2 * (rectangle.getWidth() + rectangle.getHeight());
    }
    @Override
    public double visit(Triangle triangle) {
        return triangle.getA() + triangle.getB() + triangle.getC();
    }
}
package visitor;
import java.util.List;
public class Main {
    public static void main(String[] args) {
        System.out.println("=== Visitor Design Pattern Demo ===\n");
        List<Shape> shapes = List.of(
            new Circle(5),
            new Rectangle(4, 6),
            new Triangle(3, 4, 5)
        );
        ShapeVisitor areaCalc      = new AreaCalculator();
        ShapeVisitor perimeterCalc = new PerimeterCalculator();
        System.out.printf("%-25s %12s %15s%n", "Shape", "Area", "Perimeter");
        System.out.println("-".repeat(55));
        for (Shape shape : shapes) {
            double area      = shape.accept(areaCalc);
            double perimeter = shape.accept(perimeterCalc);
            System.out.printf("%-25s %12.2f %15.2f%n", shape.name(), area, perimeter);
        }
        // Adding a NEW operation (BoundingBoxCalculator) requires zero changes
        // to Circle, Rectangle, or Triangle — just a new visitor class.
        System.out.println("\nTotal area: " +
            String.format("%.2f", shapes.stream()
                .mapToDouble(s -> s.accept(areaCalc)).sum()));
    }
}

Console Output

=== Visitor Design Pattern Demo ===

Shape                         Area         Perimeter
——————————————————-
Circle(r=5.0)                    78.54           31.42
Rectangle(4.0×6.0)               24.00           20.00
Triangle(3.0,4.0,5.0)             6.00           12.00

Total area: 108.54

Double Dispatch Explained

Java’s method overloading is resolved at compile-time based on the declared type, not the runtime type. If you pass a Shape reference to a method with overloads for Circle, Rectangle, and Triangle, Java always picks the Shape overload. Visitor solves this with two sequential dispatches: the first selects accept() based on the runtime type of the shape (via virtual method dispatch). Inside accept(), this has a known concrete type, so calling visitor.visit(this) dispatches to the correct overload. Together these two dispatches route to the right visit() implementation without any instanceof checks.

💡 Visitor vs. Pattern Matching (Java 21+): Java 21’s sealed interfaces + switch pattern matching achieve the same routing as double dispatch, without the boilerplate. If your hierarchy is sealed (all subtypes known at compile-time), switch (shape) { case Circle c -> ...; case Rectangle r -> ...; } is exhaustive, type-safe, and far less verbose than Visitor. The compiler enforces completeness. Visitor is still relevant when: (a) you target Java < 21, (b) the hierarchy is not sealed, (c) the visitor accumulates state across elements, or (d) you need to package operations as standalone objects (e.g., injected at runtime).

When to Use Visitor

Visitor is a good fit when: the element hierarchy is stable (you rarely add new types) but you frequently add new operations. The operation needs to work across multiple unrelated classes in a hierarchy. You want to gather results across elements (totals, reports, statistics) without polluting the element classes. The operation has several steps that need to be kept together for cohesion.

Visitor is the wrong tool when: you frequently add new element types — every new type requires updating every existing visitor. The element hierarchy is not stable. Your operations are simple and only concern one type — a method on the class itself is simpler. You’re on Java 21+ with sealed types — switch pattern matching is clearer.

✅ Visitor for ASTs and Compilers: Abstract Syntax Trees are the canonical use case for Visitor in production systems. The node types (IfStatement, ForLoop, MethodCall, Literal, etc.) are fixed once the language grammar is defined. But the operations on the AST multiply: type checking, constant folding, code generation, pretty-printing, dependency analysis, linting. Each is a separate visitor. The Java compiler itself, Eclipse JDT’s AST API, and ANTLR’s visitor-based parser generation all use this pattern at scale.

Runnable Code on GitHub

Full source at ankurm.com/git.app/asmhatre/design-patterns under 03-behavioral/visitor/.

javac 03-behavioral/visitor/*.java -d out/visitor
java -cp out/visitor visitor.Main

See Also

Further Reading

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.