When finding the right method, Java does not take into account the run-time type of the method's arguments. Java simply reasons about types of variables instead of values.
Solution:
Java 6 annotations can be used to annotate methods and implement multimethods and value dispatch. All this can be done at runtime without the need for any special compilation or preprocessing and the usage can still be reasonably user-friendly.
We need to introduce two "simple" annotations to annotate:
Methods
: What multimethod this method implements?
Parameters
: What value should we dispatch on?
We can then process the annotations and build a list of methods that implement a particular multimethod. This list needs to be sorted so that the most specific methods come first. "Most specific" means that for each method parameter (from left to right), the parameter type/value is more specialized (e.g. it is a subclass or it is matched agains the specified value). Calling a multimethod means invoking the most specific applicable method. "Applicable" means that the method prototype matches the actual runtime arguments and "the most specific" means that we can simply search through the sorted list and find the first one which is applicable.
Annotation processing can be wrapped up in a class which can then be used in a user defined method that will simply invoke the multimethod dispatch code with the actual runtime arguments.
Implementation
The interface Multi implements a runtime method annotation used to mark multimethods:
package jmultimethod;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Multi {
public String value();
}
The interface V implements a runtime parameter annotation used to specify dispatch values:
package jmultimethod;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface V {
public String value();
}
The Multimethod code follows:
package jmultimethod;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
public class Multimethod {
protected String name;
protected final ArrayList<Method> methods = new ArrayList<Method>();
protected final MethodComparator methodComparator = new MethodComparator();
public Multimethod(String name, Class... classes) {
this.name = name;
for(Class c: classes) {
add(c);
}
}
public void add(Class c) {
for(Method m: c.getMethods()) {
for(Annotation ma: m.getAnnotations()) {
if(ma instanceof Multi) {
Multi g = (Multi) ma;
if(this.name.equals(g.value())) {
methods.add(m);
}
}
}
}
sort();
}
protected void sort() {
Method[] a = new Method[methods.size()];
methods.toArray(a);
Arrays.sort(a, methodComparator);
methods.clear();
for(Method m: a) {
methods.add(m);
}
}
protected class MethodComparator implements Comparator<Method> {
@Override
public int compare(Method l, Method r) {
// most specific methods first
Class[] lc = l.getParameterTypes();
Class[] rc = r.getParameterTypes();
for(int i = 0; i < lc.length; i++) {
String lv = value(l, i);
String rv = value(r, i);
if(lv == null) {
if(rv != null) {
return 1;
}
}
if(lc[i].isAssignableFrom(rc[i])) {
return 1;
}
}
return -1;
}
}
protected String value(Method method, int arg) {
Annotation[] a = method.getParameterAnnotations()[arg];
for(Annotation p: a) {
if(p instanceof V) {
V v = (V) p;
return v.value();
}
}
return null;
}
protected boolean isApplicable(Method method, Object... args) {
Class[] c = method.getParameterTypes();
for(int i = 0; i < c.length; i++) {
// must be instanceof and equal to annotated value if present
if(c[i].isInstance(args[i])) {
String v = value(method, i);
if(v != null && !v.equals(args[i])) {
return false;
}
} else {
if(args[i] != null || !Object.class.equals(c[i])) {
return false;
}
}
}
return true;
}
public Object invoke(Object self, Object... args) {
Method m = null; // first applicable method (most specific)
for(Method method: methods) {
if(isApplicable(method, args)) {
m = method;
break;
}
}
if(m == null) {
throw new RuntimeException("No applicable method '" + name + "'.");
}
try {
return m.invoke(self, args);
} catch (Exception e) {
throw new RuntimeException("Method invocation failed '" + name + "'.");
}
}
}
To use multimethods, user code must:
Annotate methods with the multimethod name, e.g.
@Multi("myMultimethod")
The name of the annotated methods can be anything Java is happy with. It is most likely going to be different from the multimethod name because some methods can have prototype similar enough to cause name clashes for the Java compiler (and maybe because the compiler could have problems with the null value). Also, the method should be visible (e.g. public) to the Multimethod class.
Process the annotations by creating the Multimethod object, e.g.
protected Multimethod mm = new Multimethod("myMultimethod", getClass());
Define multimethod "entry point" method with parameters as general as necessary. This method dispatches using the Multimethod object created above, e.g.
public void myMultimethod(Object X, Object Y) {
mm.invoke(this, X, Y);
}
And then, the multimethod can be called as any normal Java method, e.g.
myMultimethod(1, null);
Limitations:
Value dispatch works only with values supported by Java annotations, e.g. values of type String.
The following code is based on the Multiple Dispatch example
package jmultimethod;
public class AsteroidTest {
class Asteroid {}
class Spaceship {}
@Multi("collide")
public void collideOO(Object X, Object Y) {
log("?? Bang, what happened? ", X, Y);
}
@Multi("collide")
public void collideAA(Asteroid X, Asteroid Y) {
log("AA Look at the beautiful fireworks! ", X, Y);
}
@Multi("collide")
public void collideAS(Asteroid X, Spaceship Y) {
log("AS Is it fatal? ", X, Y);
}
@Multi("collide")
public void collideSA(Spaceship X, Asteroid Y) {
log("SA Is it fatal? ", X, Y);
}
@Multi("collide")
public void collideSS(Spaceship X, Spaceship Y) {
log("SS Who's fault was it? ", X, Y);
}
@Multi("collide")
public void collide1S(String X, Spaceship Y) {
log("1S any string? ", X, Y);
}
@Multi("collide")
public void collide2S(@V("hi") String X, Spaceship Y) {
log("2S 'hi' value? ", X, Y);
}
protected Multimethod mm = new Multimethod("collide", getClass());
public void collide(Object X, Object Y) {
mm.invoke(this, X, Y);
}
public void run() {
Object A = new Asteroid();
Object S = new Spaceship();
collide(A, A);
collide(A, S);
collide(S, A);
collide(S, S);
collide(A, 1);
collide(2, A);
collide(S, 3);
collide(4, S);
collide(5, null);
collide(null, null);
collide("hi", S);
collide("hello", S);
}
public void log(Object... args) {
for(Object o: args) {
if(o instanceof String) {
System.out.print(" " + (String) o);
} else {
System.out.print(" " + o);
}
}
System.out.println();
}
public static void main(String[] args) throws Exception {
AsteroidTest t = new AsteroidTest();
t.run();
}
}
The program output (partially edited to fit on the screen) is:
AA Look at the beautiful fireworks! Asteroid@1f24bbbf Asteroid@1f24bbbf
AS Is it fatal? Asteroid@1f24bbbf Spaceship@24a20892
SA Is it fatal? Spaceship@24a20892 Asteroid@1f24bbbf
SS Who's fault was it? Spaceship@24a20892 Spaceship@24a20892
?? Bang, what happened? Asteroid@1f24bbbf 1
?? Bang, what happened? 2 Asteroid@1f24bbbf
?? Bang, what happened? Spaceship@24a20892 3
?? Bang, what happened? 4 Spaceship@24a20892
?? Bang, what happened? 5 null
?? Bang, what happened? null null
2S 'hi' value? hi Spaceship@24a20892
1S any string? hello Spaceship@24a20892