A pure Java multiple dispatch (multimethod) framework that selects method implementations at runtime based on the actual types of all arguments, not just the receiver.
Java's built-in virtual dispatch is single dispatch — the method called depends only on the runtime type of this. JMDispatch extends this to one or more arguments, choosing the best-matching registered handler based on all argument types simultaneously.
<dependency>
<groupId>net.eric-nicolas</groupId>
<artifactId>jmdispatch</artifactId>
<version>1.2</version>
</dependency>import net.eric_nicolas.jmdispatch.*;
public class Shapes {
@Dispatch
public static String describe(Shape s) { return "generic shape"; }
@Dispatch
public static String describe(Circle c) { return "circle r=" + c.radius; }
private static final DispatchTable table =
DispatchTable.factory(1).autoregister(Shapes.class);
public static String describe(Object obj) {
return (String) table.dispatch(obj);
}
}Calling describe(myCircle) routes to the Circle handler; passing any other Shape subclass falls back to the generic handler.
import net.eric_nicolas.jmdispatch.*;
public class Collisions {
@Dispatch
public static void collide(Shape a, Shape b) {
System.out.println("Generic shape collision");
}
@Dispatch
public static void collide(Circle a, Rectangle b) {
System.out.println("Circle-Rectangle collision");
}
private static final DispatchTable table =
DispatchTable.factory(2).autoregister(Collisions.class);
public static void handleCollision(Shape a, Shape b) {
table.dispatch(a, b);
}
}When handleCollision is called with a Circle and a Rectangle, JMDispatch routes to the specific (Circle, Rectangle) handler. If no exact match exists, it falls back to the closest ancestor match.
Handlers can be instance methods, giving them natural access to instance state:
public class CollisionHandler {
private final Logger log;
public CollisionHandler(Logger log) { this.log = log; }
@Dispatch
public void collide(Shape a, Shape b) {
log.info("Generic shape collision");
}
@Dispatch
public void collide(Circle a, Rectangle b) {
log.info("Circle-Rectangle collision");
}
}
// Register an instance — its @Dispatch methods are bound to it
DispatchTable table = DispatchTable.factory(2)
.autoregister(new CollisionHandler(myLogger));
table.dispatch(myCircle, myRect); // calls instance method on the registered handlerStatic and instance @Dispatch methods can coexist in the same class.
Handlers can return any type. The return value is boxed and returned as Object from dispatch(). Void handlers return null.
@Dispatch
public static int area(Circle c, Scale s) {
return (int)(Math.PI * c.radius * c.radius * s.factor);
}
// ...
Object result = table.dispatch(myCircle, myScale); // returns a boxed IntegerDispatchTable table = DispatchTable.factory(3)
.autoregister(MyHandlers.class); // static handlers
// or .autoregister(new MyHandlers()); // instance handlers
Object result = table.dispatch(arg1, arg2, arg3);- Annotate methods with
@Dispatch(static or instance) - Create a dispatch table and auto-register handlers from a class or instance
- Call
dispatch(...)— the framework finds the closest matching handler by computing inheritance distance across all arguments
Under the hood, JMDispatch uses ASM bytecode generation to create functor implementations that invoke your methods directly, avoiding reflection overhead at dispatch time.
- Computes inheritance distance (number of
extendssteps) from each actual argument type to each registered parameter type - Uses the Euclidean norm (sum of squared distances) to rank matches
- Selects the handler with the lowest total distance
- Throws
DispatchAmbiguousExceptionif multiple handlers tie - Throws
DispatchNoMatchExceptionif no compatible handler exists - Caches resolved dispatches for fast repeated lookups — first call pays the lookup cost, subsequent calls hit the fast exact-match path
- Handler parameter types must be concrete classes (not interfaces or abstract classes)
@Dispatchmethods must be concrete (not abstract)- All errors are typed:
InvalidDispatchExceptionat registration,DispatchNoMatchException/DispatchAmbiguousExceptionat dispatch
Concrete class hierarchies only. Dispatch operates on concrete classes, not interfaces or abstract classes. Inheritance distance is computed by walking the superclass chain (getSuperclass()), which is well-defined and unambiguous — each class has exactly one superclass path to Object. Interfaces would introduce multiple inheritance paths (diamond problem), making distance computation ambiguous and order-dependent. Since you always dispatch on actual objects (which are always instances of concrete classes), restricting handler parameter types to concrete classes is both simpler and correct.
Linear scan for exact match. The findExact() lookup uses a linear scan with pointer comparison (keys[i][0] == type1), not a HashMap. Benchmarking showed that HashMap variants (including zero-allocation ConcurrentHashMap + ThreadLocal probe keys + identity hash codes) made exact-hit dispatch 4-7x slower (2.2 → 8.8–14.2 ns/op for 2 handlers). The pointer-comparison loop is so tight that even at 20 handlers (6.8 ns) it beats the HashMap's constant overhead. The crossover point would be ~50+ handlers, which is far beyond typical use.
ASM bytecode generation. Each registered handler gets a generated functor class that calls the target method directly via invokeStatic or invokeVirtual. This avoids all reflection overhead at dispatch time — the generated functors are essentially zero-overhead wrappers.
Typed exceptions. All exceptions are typed: DispatchNoMatchException and DispatchAmbiguousException for dispatch-time errors, InvalidDispatchException for registration-time validation errors (abstract methods, interface/abstract parameter types, duplicate signatures, wrong argument count). No raw RuntimeException in user-facing paths.
The collision sample compares jmdispatch against the classic visitor pattern for the canonical double-dispatch problem: game object collision resolution where behavior depends on both object types. It implements the same 6 collision pairs (Spaceship, Asteroid, Laser) using both approaches and shows how multi-dispatch eliminates the interface ceremony, wrapper classes, and scattered reverse-dispatch methods that the visitor pattern requires.
The serialization sample tackles the 2D dispatch matrix problem: serializing domain objects (User, Product, Order) to multiple formats (JSON, XML, Binary, CSV) where behavior depends on both the object type and the target format. It compares three approaches — format logic in domain objects, instanceof chains in serializers, and multi-dispatch — showing how jmdispatch is the only solution that scales cleanly in both dimensions without modifying existing code.
@Dispatchmethods must bepublic. The ASM-generated functor classes are loaded by an internal classloader that cannot access package-private methods. Declaring handlers aspublicis the current workaround. A future fix should define generated classes in the same classloader/package as the handler, or useMethodHandles.Lookupto bypass access restrictions.
The hot path (exact-match, warm cache) benchmarks at ~2-7 ns/op depending on table size. The library is designed for dispatch-heavy patterns like event handling and message routing where this overhead is negligible.
| Class | Description |
|---|---|
@Dispatch |
Annotation to mark methods (static or instance) as dispatch handlers |
DispatchTable |
Dispatch table interface — create via DispatchTable.factory(n) where n is the number of dispatch arguments |
DispatchNoMatchException |
Thrown when no handler matches the argument types |
DispatchAmbiguousException |
Thrown when multiple handlers match with equal distance |
InvalidDispatchException |
Thrown when handler registration is invalid (abstract method, interface/abstract parameter type, etc.) |
- Java 11+
- Maven for building
mvn clean package- ASM 9.9.1 — bytecode generation (compile-time + runtime)
- JUnit 4.13.1 — testing only
This project is licensed under the GNU Lesser General Public License v3.0 — you can use it in proprietary applications, but modifications to the library itself must remain open source.