Visitor Design Pattern in Java

Learn Visitor in Java with document export and statistics, complete code, double dispatch, diagrams, and tested visitor reuse and overload behavior.

A client calls accept on an element, which calls the matching visit overload on the chosen visitor.

The Visitor pattern separates an operation from the element classes on which it operates. Each element accepts a visitor and calls the visitor method for its own concrete type. A new visitor can provide another operation across the same element types without adding that operation to each element class.

This arrangement is useful when the element types are stable but the operations change. It has a corresponding cost: adding another element type usually requires changing the visitor interface and every visitor implementation.

1. How the Visitor Pattern Works

The element interface declares an accept() method. The visitor interface declares a visit() overload for each concrete element type. An overload is a method with the same name but a different parameter list.

The client passes a visitor to an element. The element’s accept() implementation calls visitor.visit(this). Because that call appears inside a concrete element class, Java can select the overload for that concrete type. The supplied visitor then performs the operation.

A client calls an element's accept method. The concrete element calls the matching visit overload on the chosen visitor.

The roles are:

Participant Responsibility
Element Declares the operation that accepts a visitor.
Concrete element Calls the visitor overload for its own type.
Visitor Declares an operation for each concrete element type.
Concrete visitor Implements one operation across those element types.
Client Chooses a visitor and determines which elements to visit.

Dispatch means selecting the method implementation to execute. The two calls allow the final behavior to depend on both the element type and the visitor type. This is called double dispatch. Java still resolves overloaded method signatures at compile time; the pattern combines that selection with runtime method dispatch. The implementation below makes both calls visible.

2. Visitor Pattern in Action

A document editor stores text blocks and image blocks. It needs two features: export the document as plain text, and calculate document statistics. Export keeps text and substitutes an image description for each image. Statistics count text length and image blocks.

These operations belong outside the block classes so that another operation, such as an accessibility check, can be added separately. The block classes expose their content and support visiting; they do not need an export or statistics method of their own.

The example uses a flat list and stores image descriptions rather than image files. Its classes map to the pattern as follows:

Participant Document example
Element DocumentBlock
Concrete elements TextBlock and ImageBlock
Visitor DocumentVisitor
Concrete visitors TextExportVisitor and StatisticsVisitor
Client VisitorDemo

The client can apply an export visitor to a block held through the common interface:

DocumentBlock block = new ImageBlock("Team photo");
TextExportVisitor export = new TextExportVisitor();
block.accept(export);
System.out.print(export.result()); // [Image: Team photo]

The first dispatch selects ImageBlock.accept(). That method calls the image overload of visit(), which appends the description to the export. The variable’s declared type remains DocumentBlock; the caller does not cast it to an image block.

3. Implementing Document Visitors

The complete source requires JDK 17 or later and uses only standard Java APIs. Each public type below is stored in a separate file. The repository includes package declarations; the excerpts omit them.

3.1. Declaring the Two Contracts

The element contract knows how to accept a visitor, while the visitor contract lists the supported element types:

public interface DocumentBlock {
    void accept(DocumentVisitor visitor);
}
public interface DocumentVisitor {
    void visit(TextBlock block);
    void visit(ImageBlock block);
}

Every concrete visitor must implement both visit() methods. This makes the supported set explicit. If an operation does nothing for images, its image method can be empty, but that choice belongs to the operation.

3.2. Letting Each Block Select Its Overload

The blocks use Java records to hold their content. A record provides an accessor named after each component, such as text() or description(). Both blocks validate their content and implement the same accepting operation:

import java.util.Objects;

public record TextBlock(String text) implements DocumentBlock {
    public TextBlock {
        Objects.requireNonNull(text, "text is required");
    }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}
import java.util.Objects;

public record ImageBlock(String description) implements DocumentBlock {
    public ImageBlock {
        Objects.requireNonNull(description, "description is required");
    }

    @Override
    public void accept(DocumentVisitor visitor) {
        visitor.visit(this);
    }
}

Although the two accept() methods contain identical text, they select different overloads. Inside TextBlock, this has type TextBlock. Inside ImageBlock, it has type ImageBlock. The compiler therefore resolves the corresponding method in DocumentVisitor.

The record constructors use the compact syntax: parameters are supplied by the record header, and validation runs before the fields are assigned. Callers must also supply a non-null visitor to accept().

The blocks do not know whether the visitor exports text, counts elements or performs another operation. They only pass themselves to the appropriate visitor method.

3.3. Exporting Plain Text

The export visitor accumulates one line per block:

public final class TextExportVisitor implements DocumentVisitor {
    private final StringBuilder output = new StringBuilder();

    @Override
    public void visit(TextBlock block) {
        output.append(block.text()).append('\n');
    }

    @Override
    public void visit(ImageBlock block) {
        output.append("[Image: ").append(block.description()).append("]\n");
    }

    public String result() {
        return output.toString();
    }
}

A text block contributes its stored text followed by a newline. An image block contributes a bracketed description followed by a newline. result() returns the accumulated text as a String.

This is a plain-text export. It does not embed images, write a file or produce HTML. The caller decides what to do with the returned text. Newline characters already present inside a text block are preserved.

3.4. Calculating Statistics

A second visitor applies a different operation to the same blocks:

public final class StatisticsVisitor implements DocumentVisitor {
    private int textUnits;
    private int images;

    @Override
    public void visit(TextBlock block) {
        textUnits += block.text().length();
    }

    @Override
    public void visit(ImageBlock block) {
        images++;
    }

    public int textUnits() {
        return textUnits;
    }

    public int images() {
        return images;
    }
}

Text contributes its String.length() value, and each image contributes one to the image count. Image descriptions do not contribute to text length because they are stored in image blocks.

Java’s String.length() counts UTF-16 code units. That is not always the number of visible characters: some Unicode characters occupy two code units, and a displayed symbol can contain several characters. The result is therefore named textUnits, rather than a user-visible character count.

TextBlock and ImageBlock accept either TextExportVisitor or StatisticsVisitor. Export accumulates text; statistics accumulates text units and image count.

These counters use int and are intended for a small in-memory document; a larger workload needs an appropriate count range and overflow policy.

Both visitors keep mutable accumulated state. Create a fresh visitor for each independent document. Visiting a block twice appends or counts it twice; the pattern does not reset state or deduplicate elements.

4. Running the Two Operations

The client traverses the document and passes both visitors to each block:

import java.util.List;

public class VisitorDemo {
    public static void main(String[] args) {
        List<DocumentBlock> document = List.of(
                new TextBlock("Hello"),
                new ImageBlock("Team photo"),
                new TextBlock("Java"));
        TextExportVisitor export = new TextExportVisitor();
        StatisticsVisitor statistics = new StatisticsVisitor();
        for (DocumentBlock block : document) {
            block.accept(export);
            block.accept(statistics);
        }
        System.out.print(export.result());
        System.out.println("Text units: " + statistics.textUnits());
        System.out.println("Images: " + statistics.images());
        document.get(0).accept(statistics);
        System.out.println("After visiting Hello again: " + statistics.textUnits());
        StatisticsVisitor empty = new StatisticsVisitor();
        System.out.println("Empty document: " + empty.textUnits() + ", " + empty.images());
    }
}

From the cloned repository root, run:

javac -d out src/main/java/com/howtodoinjava/patterns/behavioral/visitor/*.java
java -cp out com.howtodoinjava.patterns.behavioral.visitor.VisitorDemo

The result is:

Hello
[Image: Team photo]
Java
Text units: 9
Images: 1
After visiting Hello again: 14
Empty document: 0, 0

The export preserves the order of the three blocks. The statistics visitor counts five text units for “Hello” and four for “Java,” giving nine. The image is counted separately.

Visiting “Hello” again adds another five units to the same visitor, producing fourteen. The fresh visitor used for an empty document remains at zero for both counters. These cases show why traversal and visitor lifetime are application decisions.

4.1. Why a Direct Overloaded Call Does Not Work

Replacing the accepting call with a direct call does not preserve the dispatch behavior:

DocumentBlock block = new TextBlock("Hello");
DocumentVisitor visitor = new StatisticsVisitor();
visitor.visit(block); // Does not compile

Neither declared overload accepts DocumentBlock. The compiler selects an overload using the argument’s compile-time type; it does not choose visit(TextBlock) by inspecting the instance stored in block at runtime.

Calling block.accept(visitor) solves this in two stages. Runtime dispatch selects TextBlock.accept(). Inside that method, the type of this is known to be TextBlock, so the matching overload is available. Java’s method invocation rules distinguish overload resolution from locating the method implementation at runtime.

5. When Visitor Is Worth the Extra Structure

Visitor is useful when several operations work across the same stable set of element types. Its main tradeoff is which kind of change becomes easier:

Change Required work
Add an operation Add a visitor that implements the existing methods. Block classes stay unchanged.
Add a block type Add the block and a visitor overload, then update every visitor implementation.
Change traversal Update the client or traversal component. Visitors need not own the iteration.

If there is only one operation and it naturally belongs to each element, ordinary instance methods can be simpler. Strategy selects a behavior without requiring one overload per element type. Composite can provide a nested document structure, but combining it with Visitor still requires an explicit traversal policy.

A visitor also needs access to the element data required by its operation. It does not bypass private fields. Adding accessors solely for a visitor can weaken encapsulation, so consider that cost alongside the convenience of adding operations.

6. Frequently Asked Questions

The accepting operation, traversal and visitor state have separate responsibilities. These distinctions also matter when adapting the pattern to existing code.

6.1. Can a Visitor Work with Unchanged Third-Party Classes?

The classic arrangement requires the elements to expose accept(). A visitor cannot add that method to a third-party type. If the type cannot change, a wrapper or a different dispatch mechanism is needed. Simply creating overloaded methods is not equivalent to the accepting arrangement shown here.

6.2. Does Visitor Automatically Traverse a Tree?

No. This example’s client loop chooses the elements and their order. A tree-based document could use a separate walker or let composite elements visit their children. Decide who owns traversal so children are neither skipped nor visited twice.

6.3. Can the Same Visitor Be Shared Between Threads?

The visitors here are mutable and have no synchronization. Use separate instances for independent work. Sharing a visitor would require protecting both its accumulated state and the ordering of visits; making one counter atomic would not make the text export ordered or correct by itself.

7. Conclusion

Choose Visitor when operations change more often than the supported element types. Keep the accepting methods small, define which component traverses the elements, and make the visitor’s state lifetime explicit. When new element types are frequent, the cost of updating all visitors may outweigh the benefit.

Happy Learning!

The complete example is available in the GitHub source folder.

Leave a Comment

  1. Just a thought. In Java 8 you could use default method in Router interface, so that you don’t have to repeat implementation on all Router implementations.

    public interface Router
    {
    public void sendData(char[] data);
    public void acceptData(char[] data);

    default public void accept(RouterVisitor v) {
    v.visit(this);
    }
    }

    • Awesome thought !! A really useful update. Thanks for suggesting. But at this point, I would not like to update the existing source because Java 8 still is not adopted in most of the organizations for production (in my knowledge).

    • Nice post. Many Thanks.
      One question. About comment Mladen

      public interface Router
      . . . . . .
      default public void accept(RouterVisitor v)
      {
      v.visit(this); // at this moment this is type of is Router
      }

      And RouterVisitor hast NOT method visit(Router router)

      public interface RouterVisitor {
      public void visit(DLinkRouter router);
      public void visit(TPLinkRouter router);
      public void visit(LinkSysRouter router);
      }

      DLinkRouter is a Router
      but
      Router NOT is a DlinkRouter
      NOT is a TPLinkRouter
      NOT is a LinkSysRouter

      What do you think about?

  2. Hi Lokesh,

    I like all your topic discussions,
    I want to know template pattern in detail (am writing here as it is also comes under behavioural pattern ).
    Please discuss about ‘Template pattern’ if you have some time.
    Thanks in advance.

  3. looked around for good explanation/usecase of visitor design pattern. I must admit this is far the best example/explanation. Kudos to the author!

  4. RouterVisitor should take Router interface as dependency. You are using the concrete Router classes which violates the SOLID DI principle..

    so RouterVisitor should have only 1 method visit(Router router)

    In Each Concrete Configurator class, we can implement visit method with Reflection (for all RouterVisitor types)

  5. If we introduced new router, again we need to modify all the visitor right since visit() method is getting Visitable class not interface. Again this will make big issue in OPEN/CLOSED principle. why should not we use interface and print the message based on the object type. please correct me if i m wrong.

    • Yes you are right. New router will have to be configured for each environment and it will change lots of classes, but isn’t it how it should be done?? It forces the developer to handle new router in each supported environment.

      • Don’t you think that if we don’t use visitor here then also we are forcing the developer to handle new router in each supported environment.

  6. As per definition if we modify structure of any object that should not effect on others…in your example let i need to add one new method in DLinkRouter class then how we are going to handle that…?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.