Double Dispatching in Java
Overview
This is an example and explanation of double dispatching in Java, just to add to the collection :-)
The following is the entrypoint to the thread on this topic:
Original Posting: Double-dispatching in Java
Roedy Green wrote:
> >double dispatching
>
> Would anyone care to post a small code snippet, preferably in Java or
> C++, that demonstrates what double dispatching is?
OK, I will give a relatively concise one in Java. Please see the GOF
Design Patterns Book, The Smalltalk Design Patterns Companion, Kent
Beck's Best Practice Patterns, and so on for more information and more
detailed examples.
Say I need a 2D-Point with highly accurate X and Y numeric values:
import java.math.*;
class Point2D implements Addable {
protected final BigDecimal myX;
protected final BigDecimal myY;
public Point2D(BigDecimal x, BigDecimal y) {
myX = x;
myY = y;
}
public BigDecimal getX() {
return myX;
}
public BigDecimal getY() {
return myY;
}
public String toString() {
return "";
}
}
I certainly would like to be able to add points together:
public Point2D addToPoint(Point2D aPoint) {
return new Point2D(
this.getX().add(aPoint.getX()),
this.getY().add(aPoint.getY())
);
}
And I decide I would also like to be able to add numbers to points. I
will consider the number to be just an X value of a "virtual" point (so
the Y value is considered '0'):
public Point2D addToBigDecimal(BigDecimal aBigDecimal) {
return new Point2D(
getX().add(aBigDecimal),
getY()
);
}
Nothing too complex and no double-dispatching. I can test it with:
class PointTest {
static public void main(String[] args) {
Point2D p1 = new Point2D(new NewBigDecimal("5"), new NewBigDecimal("6"));
Point2D p2 = new Point2D(new NewBigDecimal("3.1"), new NewBigDecimal("4.5"));
NewBigDecimal bd1 = new NewBigDecimal("7.2");
NewBigDecimal bd2 = new NewBigDecimal("9.1");
System.out.println("P1 = " + p1);
System.out.println("P2 = " + p2);
System.out.println("BD1 = " + bd1);
System.out.println("BD2 = " + bd2);
System.out.println();
System.out.println("*Results*");
Object result1a = p1.addToPoint(p2);
Object result2a = p1.addToBigDecimal(bd1);
System.out.println("P1+P2 = "+result1a);
System.out.println("P1+BD1 = "+result2a);
}
}
Now I decide to keep a collection of offsets/movements from some
starting point. These offsets could either be a 2DPoint or a number
(because they only shift the X axis). I simply want to add either
points or numbers to a point and for the result to be correct:
Object result3a = p1.addTo(p2).addTo(bd1);
I just add two new methods:
public Point2D addTo(NewBigDecimal value) {
return addToBigDecimal(value);
}
public Point2D addTo(Point2D value) {
return addToPoint(value);
}
I have now collapsed my longer names into shorter (overloaded) names and
so can ignore whether a given argument is a point or a number.
Cool, I now have short operation names, and the program will choose
between them based upon... No, wait... the only reason the above
'result3a' works is because I told the compiler what types the variables
'p2' and 'bd1' are. If I am getting these objects out of a collection,
I will not know what type of object they are specifically, I will only
know they an be added to a Point. The compiler can not tell me the
difference, only the runtime object can. If both a1 and a2 are
extracted from an Enumeration, the following:
Object a1, a2;
Object result3b = p1.addTo(a1).addTo(a2);
is unresolvable unless 'addTo' is more generic.
OK, wrong path (stupid overloading), so I switch to a more generic
'addTo' and a type check:
public Point2D addTo(Object value) {
if (anObject instanceof NewBigDecimal) return addToBigDecimal(value);
if (anObject instanceof Point2D) return addToPoint(value);
//....
}
Ohhh.... yich... I don't really want to couple myself to specific
classes -- I would have to keep changing Point2D::addTo over and over
again as new classes are added -- I just want this object and its value
to "do the right thing" depending on the capabilities of 'value'. Well,
I have gotten halfway there... I know who I am so I can just delegate to
my value to finish the process:
public Point2D addTo(Object value) {
return value.addToPoint(this);
}
EUREKA, that's it... :-) I have now dispatched both on myself (to get to
the Point2D::addTo method) and on 'value'. We have a double dispatch
and
Object result3b = p1.addTo(a1).addTo(a2);
will work correctly whether a1 or a2 is a Point or a number. That one
line:
return value.addToPoint(this);
is all there is to a double dispatch in concept.
Except we are in Java and I slipped in a bit of "optimistic" untyped
code and left out a little of the implementation. Objects in Java would
not know how to handle 'addToPoint' and Java would not know that
'addToPoint' returns a Point2D. Unfortunately, things get even messier
VERY quickly with Java because it does not have Covariant return types,
we can not augment existing classes, and so on. The following is a
fuller set of code to make the above really work:
/**
An addable is an object that can be added to another object. The exact
meaning of that addition
depends on the two objects involved.
**/
interface Addable {
Addable addTo(Addable anObject);
/**
*This is really "private" within the Addable implementations
*/
Addable addToBigDecimal(NewBigDecimal aBigDecimal);
/**
*This is really "private" within the Addable implementations
*/
Addable addToPoint(Point2D aPoint);
}
/**
NewBigDecimal is simply the BigDecimal class with the additional
operations
of the Addable interface.
We just want to augment BigDecimal with 'addTo' methods, but Java does
not support augmentation of existing classes so we have to create our
own subclass.
**/
class NewBigDecimal extends java.math.BigDecimal implements Addable {
public NewBigDecimal(String val) throws NumberFormatException {
super(val);
}
public Addable addTo(Addable addable) {
return addable.addToBigDecimal(this);
}
public Addable addToBigDecimal(NewBigDecimal aBigDecimal) {
//Hack to handle the class conversion between BigDecimal and NewBigDecimal
return new NewBigDecimal(this.add(aBigDecimal).toString());
}
public Addable addToPoint(Point2D aPoint) {
//just reverse the call because Points are more interesting then BigDecimals
return aPoint.addToBigDecimal(this);
}
}
import java.math.*;
class Point2D implements Addable {
final BigDecimal myX;
final BigDecimal myY;
public Point2D(BigDecimal x, BigDecimal y) {
myX = x;
myY = y;
}
public Addable addTo(Addable addable) {
return addable.addToPoint(this);
}
public Addable addToBigDecimal(NewBigDecimal aBigDecimal) {
return new Point2D(
getX().add(aBigDecimal),
getY()
);
}
public Addable addToPoint(Point2D aPoint) {
return new Point2D(
this.getX().add(aPoint.getX()),
this.getY().add(aPoint.getY())
);
}
public BigDecimal getX() {
return myX;
}
public BigDecimal getY() {
return myY;
}
public String toString() {
return "";
}
}
class PointTest {
static public void main(String[] args) {
Point2D p1 = new Point2D(new NewBigDecimal("5"), new NewBigDecimal("6"));
Point2D p2 = new Point2D(new NewBigDecimal("3.1"), new NewBigDecimal("4.5"));
NewBigDecimal bd1 = new NewBigDecimal("7.2");
NewBigDecimal bd2 = new NewBigDecimal("9.1");
Addable a1 = p2;
Addable a2 = bd1;
Addable a3 = bd2;
System.out.println("P1 = " + p1);
System.out.println("P2 = " + p2);
System.out.println("BD1 = " + bd1);
System.out.println("BD2 = " + bd2);
System.out.println();
System.out.println("A1 = " + a1);
System.out.println("A2 = " + a2);
System.out.println("A3 = " + a3);
System.out.println();
System.out.println("*Results*");
Object result1a = p1.addTo(p2);
Object result2a = p1.addTo(bd1);
Object result3a = p1.addTo(p2).addTo(bd1);
Object result1b = p1.addTo(a1);
Object result2b = p1.addTo(a2);
Object result3b = p1.addTo(a1).addTo(a2);
Object result4 = a2.addTo(a1);
Object result5 = a3.addTo(a2);
System.out.println("P1+P2 = "+result1a);
System.out.println("P1+BD1 = "+result2a);
System.out.println("P1+P2+BD1 = "+result3a);
System.out.println("P1+A1 = "+result1b);
System.out.println("P1+A2 = "+result2b);
System.out.println("P1+A1+A2 = "+result3b);
System.out.println("A2+A1 = "+result4);
System.out.println("A3+A2 = "+result5);
}
}
Some C++ examples and relatively recent discussions are at:
--Mark
mark.fussell@chimu.com
Subsequent Discussions
Adding more classes
Steve Wart wrote:
> Mark Fussell wrote in message <36B648A9.F98571CC@chimu.com>...
> >Roedy Green wrote:
> >> Would anyone care to post a small code snippet, preferably in Java or
> >> C++, that demonstrates what double dispatching is?
> >
> >OK, I will give a relatively concise one in Java. Please see the GOF
> >Design Patterns Book, The Smalltalk Design Patterns Companion, Kent
> >Beck's Best Practice Patterns, and so on for more information and more
> >detailed examples.
> >
> [ amazingly comprehensive example deleted ]
Why thank you :-)
> What happens as more classes need to implement the Addable interface?
> Do things get more complex in a combinatorial way?
It isn't combinatorial because you only have a single entry per type of
object that you will need for the second dispatch. The number of
dispatching operations could be equal to the number of implementation
classes (times the number of "actual" operations), but it is likely to
be somewhat smaller than that if there is no externally important
difference for the particular type's perspective. For example, you
could have Colored-2d-Points without extending the the Addable
interface.
It does cause a focal point (bottleneck) in that Addable interface: you
have to have the union of all dispatching operations within it, so it
will be hard to "close" it against future changes. The other option is
to keep creating subtypes of the interface ("MoreAddable extends
Addable") that have new capabilities, but this then returns us to the
starting problem of needing to behave differently depending on what the
type of the argument is. Say MoreAddable understands ColoredPoints
where Addable only understands Points. Then the staticly bound version
would be:
public ColoredPoint2D addTo(Addable value) {
return value.addToPoint(this);
}
public ColoredPoint2D addTo(MoreAddable value) {
return value.addToColoredPoint(value);
}
And so the dynamically bound version would have to do a third dispatch
on Addable/MoreAddable or (more likely) a quick typecheck to see if a
'MoreAddable' value was received.
public Point2D addTo(Addable value) {
if (anObject instanceof MoreAddable) {
return value.addToColoredPoint(value);
} else {
return value.addToPoint(this);
}
}
Which returns us to an ugly (but hopefully unusual) typecheck throughout
the code.
> Is this what people are talking about when
> they speak of the strengths of strong typing or is this a contrived example to make a Java
> implementation look much more complex than the Smalltalk equivalent?
I don't think it is a contrived example: this is exactly what the
Visitor pattern relies on and the need for that pattern is much more
common in strongly typed environments than in Smalltalk. The advantage
of the Java version vs. a Smalltalk equivalent is that the possible
intercommunication between objects is completely identified and
documented. Its disadvantage is that there is a significant increase in
work as the possible interactions (including different granularities of
operation-bundles [e.g. Types]) increase.
Strong typing certainly becomes more "cumbersome" as flexibility needs
to increase and the runtime objects determine more and more of the
desired behavior. Generally, this just discourages producing these
types of very generic structures (e.g. no general "Magnitude" / Addable
in Java).
> Or is it really not that bad, I am just so used to Smalltalk that it seems worse than it is?
It's not *that* bad but it certainly can be bad. Just a lot more work
to do what might be a very simple (in Smalltalk) concept. A more
sophisticated typed language like Eiffel is a bit better, and
"augmentation" (Envy extensions) of existing classes would help a lot,
but the tradeoff is always there.
--Mark
mark.fussell@chimu.com
Difficulty of double-dispatch in Java compared to Dylan
Peter wrote:
> Mark Fussell wrote:
> > OK, I will give a relatively concise one in Java.
>
> [huge 9,300 character example deleted]
>
> I'd hate to see a verbose example. :-)
Well, to be fair, more than half of those characters were actually my
narrative. And of the rest, half of it was a test program. The lines
involved in the double dispatch for Java were:
{Within Addable}
Addable addToBigDecimal(NewBigDecimal aBigDecimal);
Addable addToPoint(Point2D aPoint);
{Within NewBigDecimal}
public Addable addTo(Addable addable) {
return addable.addToBigDecimal(this);
}
{Within Point2D}
public Addable addTo(Addable addable) {
return addable.addToPoint(this);
}
For a total of 8 lines, two of which are an interface declaration. The
equivalent in Smalltalk would be 6 lines because the interface
declaration is not needed. Obviously in Dylan and similar multi-method
languages there would be exactly zero lines to do the double dispatch.
Of course the double dispatching itself is only a portion of the actual
functionality (adding Points to Points and Points to Numbers), although
it is much more significant than normal in this particularly simple and
focused example.
> It seems like an exercise in frustration to simulate a limited form of
> multi-method disptach in these mono-dispatch language. Why settle for a
> substitute when you can have the real thing.
This is a more interesting topic: what are the advantages and
disadvantages of changing to a multi-dispatch form over a
single-dispatch form. Since I have a version of multi-dispatching in
Smalltalk, I might as well try to comment on that, but it is a longer
and separate post.
[[Rest of discussion snipped... tangential topic]]
--Mark
mark.fussell@chimu.com
|