Skip to main content

Visitor Design Pattern in Java: Complete Guide with Examples

The Visitor pattern lets you add new operations to a class hierarchy without modifying it. Each operation is a separate Visitor class; elements accept visitors via double dispatch. Complete Java guide: shape hierarchy with AreaCalculator and PerimeterCalculator visitors, double dispatch explained, when Visitor beats a switch statement, and when it’s overkill.

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.

All code compiles and runs with Java 25. No external dependencies required.

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

The visitor interface declares one overload per concrete shape type:

ShapeVisitor.java
package visitor;

/** Visitor interface — one visit() method per concrete element type */
public interface ShapeVisitor {
    void visit(Circle circle);
    void visit(Rectangle rectangle);
    void visit(Triangle triangle);
}

Every shape implements accept(), which is the double-dispatch trick: it hands itself to the visitor as a concretely-typed this, so the visitor’s overload resolution picks the right method:

Shape.java
package visitor;

/** Element interface — accepts a visitor */
public interface Shape {
    void accept(ShapeVisitor visitor);
    String getName();
}

Circle is the first concrete element:

Circle.java
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 getName() { return "Circle(r=" + radius + ")"; }
    @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); }
}

Rectangle follows the identical shape — a couple of dimensions, a name, and the same one-line accept():

Rectangle.java
package visitor;

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 getName() { return "Rectangle(" + width + "x" + height + ")"; }
    @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); }
}

Triangle is modelled by base and height — none of the three shape classes needs to know anything about area or perimeter:

Triangle.java
package visitor;

public class Triangle implements Shape {
    private final double base, height;
    public Triangle(double base, double height) { this.base = base; this.height = height; }
    public double getBase()   { return base; }
    public double getHeight() { return height; }
    @Override public String getName() { return "Triangle(b=" + base + ",h=" + height + ")"; }
    @Override public void accept(ShapeVisitor visitor) { visitor.visit(this); }
}

AreaCalculator is the first concrete visitor. Every area formula lives here — not scattered across the three shape classes:

AreaCalculator.java
package visitor;

/**
 * Concrete Visitor 1 — calculates area for each shape type.
 * Area formulas live here, not scattered across shape classes.
 */
public class AreaCalculator implements ShapeVisitor {

    @Override
    public void visit(Circle c) {
        double area = Math.PI * c.getRadius() * c.getRadius();
        System.out.printf("  Area of %s = %.2f%n", c.getName(), area);
    }

    @Override
    public void visit(Rectangle r) {
        double area = r.getWidth() * r.getHeight();
        System.out.printf("  Area of %s = %.2f%n", r.getName(), area);
    }

    @Override
    public void visit(Triangle t) {
        double area = 0.5 * t.getBase() * t.getHeight();
        System.out.printf("  Area of %s = %.2f%n", t.getName(), area);
    }
}

PerimeterCalculator is the second concrete visitor. Adding it required zero changes to Shape, Circle, Rectangle, or Triangle — that’s the whole point of the pattern:

PerimeterCalculator.java
package visitor;

/**
 * Concrete Visitor 2 — calculates perimeter.
 * Adding this visitor added zero code to Shape, Circle, Rectangle, Triangle.
 */
public class PerimeterCalculator implements ShapeVisitor {

    @Override
    public void visit(Circle c) {
        double p = 2 * Math.PI * c.getRadius();
        System.out.printf("  Perimeter of %s = %.2f%n", c.getName(), p);
    }

    @Override
    public void visit(Rectangle r) {
        double p = 2 * (r.getWidth() + r.getHeight());
        System.out.printf("  Perimeter of %s = %.2f%n", r.getName(), p);
    }

    @Override
    public void visit(Triangle t) {
        // Simplified: assume isoceles — base + 2 equal sides approximated
        double side = Math.sqrt(Math.pow(t.getBase() / 2, 2) + Math.pow(t.getHeight(), 2));
        double p = t.getBase() + 2 * side;
        System.out.printf("  Perimeter of %s = %.2f%n", t.getName(), p);
    }
}

Main runs both visitors across the same three shapes, then prints why adding a third visitor would be just as cheap:

Main.java
package visitor;

import java.util.List;

/**
 * Visitor Design Pattern — Runnable Demo
 * Run: javac visitor/*.java -d out/visitor && java -cp out/visitor visitor.Main
 * Article: https://ankurm.com/visitor-design-pattern-java/
 */
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, 8)
        );

        System.out.println("-- Area Calculator Visitor --");
        ShapeVisitor areaCalc = new AreaCalculator();
        for (Shape shape : shapes) {
            shape.accept(areaCalc);
        }

        System.out.println("\n-- Perimeter Calculator Visitor --");
        ShapeVisitor perimCalc = new PerimeterCalculator();
        for (Shape shape : shapes) {
            shape.accept(perimCalc);
        }

        System.out.println("\n-- Key insight --");
        System.out.println("Added PerimeterCalculator without touching Shape, Circle, Rectangle, Triangle.");
        System.out.println("To add another operation: write one new Visitor class.");

        System.out.println("\n=== Demo complete ===");
    }
}

Console Output

=== Visitor Design Pattern Demo ===

— Area Calculator Visitor —
  Area of Circle(r=5.0) = 78.54
  Area of Rectangle(4.0×6.0) = 24.00
  Area of Triangle(b=3.0,h=8.0) = 12.00

— Perimeter Calculator Visitor —
  Perimeter of Circle(r=5.0) = 31.42
  Perimeter of Rectangle(4.0×6.0) = 20.00
  Perimeter of Triangle(b=3.0,h=8.0) = 19.28

— Key insight —
Added PerimeterCalculator without touching Shape, Circle, Rectangle, Triangle.
To add another operation: write one new Visitor class.

=== Demo complete ===

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

No Comments yet!

Leave a Reply

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