Picture a Flipkart order. The cart has three kinds of items: electronics, groceries and clothing. On day one you only need the price. Then finance wants GST. Then logistics wants shipping cost. Then accounts wants an invoice export. Then marketing wants loyalty points.
The item types barely change. The list of things you do to those items keeps growing. Visitor is built for exactly this.
What it is
Visitor lets you add a new operation to a group of classes without editing those classes. Each operation gets its own "visitor" class. The visitor has one method for each element type (an element is one of the classes being visited, such as Electronics). The elements need only one small method, accept(visitor), which hands the element to the visitor.
In one line: move the operations out, keep the data in.
Real-life analogy
Think of an income tax officer visiting different shops in a market. A kirana store, a jewellery shop and a pharmacy all get visited. The officer knows the rules for each kind of shop. The shops don't need to learn tax law. They just open the door and say "come in" (accept).
Next month a fire safety inspector visits the same shops with a different checklist. The shops still don't change. You just sent a different visitor.
The problem
Here is what the code looks like before Visitor (full code in Before/Main.java):
abstract class CartItem {
abstract double gst(); // finance team's rules
abstract double shippingCost(); // logistics team's rules
abstract String invoiceLine(); // accounts team's rules
}Every item class implements all three. Three things go wrong:
- Every new operation touches every item class. Loyalty points means editing
Electronics,Grocery,Clothingand the base class. - Unrelated rules sit together. GST logic and shipping logic live side by side inside
Grocery. Two teams keep editing the same file. - People give up and write
instanceofchains. (instanceofchecks which class an object belongs to.) Someone adds aLoyaltyPointsCalculatorwithif (item instanceof Electronics) ... else if .... Later aBookstype is added, and the chain quietly returns 0 points for it. There is no compile error, just a wrong number in production.
Class diagram
This diagram shows the After version, where CartItem is an interface. In the Before code it is an abstract class.
| Component | Job |
|---|---|
Element (CartItem) | Declares accept(visitor). |
Concrete elements (Electronics, Grocery, Clothing) | Hold the data. accept just calls visitor.visit(this). |
Visitor (ItemVisitor<R>) | One visit method per element type. R is what the operation returns. |
Concrete visitors (GstCalculator, ShippingCalculator, ...) | One class per operation. All the rules for that operation, for every item type, in one file. |
Client (Main) | Loops over the items and passes in whichever visitor it needs. |
Double dispatch, in plain words
This is the hard part, so follow one electronics item instead of memorising the term. The client has a variable declared as CartItem, but the object inside is really an Electronics. It also passes an ItemVisitor<Double> whose real object is a GstCalculator. We need both real types to reach GstCalculator.visit(Electronics).
Java makes two related choices:
- Overriding (a subclass replaces a method) picks the version at runtime, from the real object that receives the call. So
item.accept(gst)runsElectronics.accept, even though the variable is declared asCartItem. - Overloading (several methods share a name but take different parameter types) picks the version at compile time, from the declared type of the argument. A direct
visitor.visit(item)sees onlyCartItem. Java cannot guess that the object will be anElectronicslater.
The one-line accept method supplies the missing type information:
item.accept(gst)dispatches toElectronics.acceptbecause the item object is anElectronics.- Inside that method,
thisis statically known to beElectronics, sovisitor.visit(this)selects thevisit(Electronics)signature. - The visitor object is really a
GstCalculator, so Java runs that class's override ofvisit(Electronics).
That two-stop route, first through the element and then through the visitor, is called double dispatch. It is not magic. The first call reveals the element type, so the second call can reach the matching visitor method.
How it solves the problems
| Before | After |
|---|---|
| New operation edits every item class | New operation is one new visitor class. Items are untouched. |
| GST and shipping rules mixed inside each item | Each visitor holds one operation for all item types |
instanceof chains that fail silently | Add a visit(Books) to the interface and every visitor that forgot it fails to compile |
In the After code, adding loyalty points is just this:
class LoyaltyPointsCalculator implements ItemVisitor<Integer> {
public Integer visit(Electronics item) { return (int) (item.getPrice() / 100); }
public Integer visit(Grocery item) { return (int) (item.getPrice() / 50); }
public Integer visit(Clothing item) { return (int) (item.getPrice() / 75); }
}The trade-off you must say out loud
Visitor makes one kind of change cheap and the other kind costly.
| Change | Without Visitor | With Visitor |
|---|---|---|
| Add a new operation (loyalty points) | Edit every item class | Add one class |
| Add a new element type (Books) | Add one class | Edit the visitor interface and every visitor |
So use it only when the element types are stable and the operations keep growing. If the product team adds a new item type every sprint, Visitor will hurt more than it helps.
There is a second cost. A visitor needs to read the element's data, so elements often expose getters they would otherwise keep private. Some encapsulation (keeping data hidden inside the class) is lost.
The modern alternative: sealed types and pattern matching
From Java 21 you can get the same safety with much less code. Seal the hierarchy (list every allowed subtype) and use a pattern matching switch (a switch that picks a branch by the object's type):
sealed interface CartItem permits Electronics, Grocery, Clothing {}
record Electronics(String name, double price, int warrantyMonths) implements CartItem {}
record Grocery(String name, double price, double weightKg) implements CartItem {}
record Clothing(String name, double price, String size) implements CartItem {}
static double gst(CartItem item) {
return switch (item) {
case Electronics e -> e.price() * 0.18;
case Grocery g -> g.price() * 0.05;
case Clothing c -> c.price() * (c.price() <= 2500 ? 0.05 : 0.18);
};
}No default branch is needed, because the compiler knows every permitted type. Add Books to the permits list and every switch that misses it stops compiling. Visitor gives the same guarantee, but this needs no accept methods or visitor interface.
For new code on Java 21+, this is usually the better choice. Visitor still matters when you are on older Java, when you use libraries built around visitors, or when the structure is a deep tree. For a deep tree, a base class like javac's TreeScanner does the walking, and your visitor overrides only the two or three node types it cares about. A switch does not do that walking for you.
Neither approach handles an open hierarchy well. If plugins can add new element types at runtime, Visitor cannot help. Nobody can add a visit method for a type the interface has never heard of. More on sealed types in Records and Sealed Classes.
(GST rates in these examples are simplified. Real GST depends on HSN codes.) The sample uses double so the Visitor steps stay easy to see. Production money code should normally use BigDecimal with an explicit scale and RoundingMode. Changing the number type does not change double dispatch or the Visitor design.
Pros and cons
Pros
- New operations don't touch existing classes (Open/Closed Principle for operations).
- All the logic for one operation sits in one class, easy to test and easy to own.
- The compiler tells you when a visitor misses an element type.
- A visitor can carry state while it walks a structure, like a running total or an indentation level.
Cons
- Adding a new element type is painful.
- Elements have to expose their data through getters.
acceptand double dispatch confuse people new to the code.- Overkill for two types and one operation.
- Harder to debug. A stack trace bounces from the loop to
accepttovisit, and a reader must know the trick to follow it.
When to use it
- The set of element types is stable, but new operations show up often.
- You have a tree or object structure (an AST, a document model, a cart) and want to run many unrelated operations over it.
- The operations belong to different teams or modules and shouldn't live inside the domain classes.
When not to use it
- New element types are added often.
- You only have one or two operations. Plain polymorphism (a method on each class) is simpler.
- You are on Java 21+ with a closed hierarchy. Reach for sealed types and
switchfirst.
Real-world places you've seen it
javax.lang.model: annotation processors (tools that run while Java code compiles) useElementVisitorandTypeVisitor, with helpers likeSimpleElementVisitor14, to walk classes, methods and fields.- javac's Compiler Tree API:
com.sun.source.tree.TreeVisitorandTreeScannerwalk the syntax tree of Java source. - ASM bytecode library:
ClassReaderwalks a.classfile and calls yourClassVisitorandMethodVisitorfor each part. Spring ships a repackaged copy of ASM to read class metadata without loading the class, and Byte Buddy (which Mockito uses) is built on it. - ANTLR: can generate a
<Grammar>BaseVisitorclass for your grammar so you can walk the parse tree. - Compilers and linters in general: type checking, constant folding and code generation are usually separate visitors over the same AST.
java.nio.file.FileVisitorwithFiles.walkFileTree: it borrows the name. It is closer to a callback than true double dispatch, but the idea of "walk a structure, run my logic at each node" is the same.
Common mistakes
- Using it when types change often. You will edit ten visitors every time someone adds a product type.
- Putting
instanceofinside a visitor. If you need it, the double dispatch is not set up right. - Forgetting
acceptin a new subclass. SayElectronicshas a subclassLaptopthat does not overrideaccept. ALaptopis then visited as a plainElectronics. Sometimes that is fine and sometimes it is a bug. - Making
acceptdo work.acceptshould only callvisitor.visit(this). Business logic belongs in the visitor. - Returning
voidand storing results in fields. A genericItemVisitor<R>that returns a value is cleaner and easier to test.
Interview questions
"What problem does the Visitor pattern solve?"
It lets you add new operations to a set of classes without changing those classes. Each operation becomes a visitor class with one
visitmethod per element type, and the elements only implementaccept(visitor).
"What is double dispatch and why does Visitor need it?"
Java picks the override by the object's runtime type but picks the overload by the argument's compile-time type. So
visitor.visit(item)withitemdeclared asCartItemcan't reachvisit(Electronics). Visitor callsitem.accept(visitor)first, which runs the right element class, and inside itvisitor.visit(this)has the exact type, so the right overload and the right visitor both get picked.
"What is the main trade-off of Visitor?"
Adding operations becomes easy, but adding element types becomes hard. A new element type means a new method on the visitor interface and a change to every existing visitor.
"When would you avoid Visitor?"
When element types change often, when there are only one or two operations, or when you are on Java 21+ with a closed hierarchy where sealed types plus a pattern matching
switchdo the same job with less code.
"How do sealed interfaces and pattern matching compare to Visitor?"
A
switchover a sealed interface is checked for exhaustiveness by the compiler, so a missing case fails to compile, which is the main safety Visitor gives. It needs noacceptmethods or visitor interface, and the operation is just a function.
"Why not just use an instanceof chain?"
Because the compiler doesn't check it. Add a new type and the chain falls through to a default branch and gives a wrong answer at runtime. With Visitor, the new
visitmethod on the interface forces every visitor to handle it.
"Where is Visitor used in real Java code?"
Compilers and tools that walk syntax trees, like
javax.lang.modelelement visitors in annotation processors, javac'sTreeVisitor, ASM'sClassVisitorfor bytecode, and ANTLR's generated parse tree visitors.