关于为什么Java是单派发以及Common|关于为什么Java是单派发以及Common Lisp又伟大了一次的这档子事

众所周知,在 Java 语言中支持基于子类型的多态,例如某百科全书中就给了一个基于Animal及其两个子类的例子(代码经过我微微调整)

abstract class Animal { abstract String talk(); }class Cat extends Animal { String talk() { return "Meow!"; } }class Dog extends Animal { String talk() { return "Woof!"; } }public class Example { static void letsHear(final Animal a) { System.out.println(a.talk()); }public static void main(String[] args) { letsHear(new Cat()); letsHear(new Dog()); } }

【关于为什么Java是单派发以及Common|关于为什么Java是单派发以及Common Lisp又伟大了一次的这档子事】基于子类型的多态要求在程序的运行期根据参数的类型,选择不同的具体方法——例如在上述例子中,当方法letsHear中调用了参数a的方法talk时,是依照变量a在运行期的类型(第一次为Cat,第二次为Dog)来选择对应的talk方法的实例的,而不是依照编译期的类型Animal
但在不同的语言中,在运行期查找方法时,所选择的参数的个数是不同的。对于 Java 而言,它只取方法的第一个参数(即接收者),这个策略被称为 single dispatch。
Java 的 single dispatch 要演示为什么 Java 是 single dispatch 的,必须让示例代码中的方法接收两个参数(除了方法的接收者之外再来一个参数)
// 演示 Java 是 single dispatch 的。 abstract class Shape {}class Circle extends Shape {}class Rectangle extends Shape {}class Triangle extends Shape {}abstract class AbstractResizer { public abstract void resize(Circle c); public abstract void resize(Rectangle r); public abstract void resize(Shape s); public abstract void resize(Triangle t); }class Resizer extends AbstractResizer { public void resize(Circle c) { System.out.println("缩放圆形"); } public void resize(Rectangle r) { System.out.println("缩放矩形"); } public void resize(Shape s) { System.out.println("缩放任意图形"); } public void resize(Triangle t) { System.out.println("缩放三角形"); } }public class Trial1 { public static void main(String[] args) { AbstractResizer resizer = new Resizer(); Shape[] shapes = {new Circle(), new Rectangle(), new Triangle()}; for (Shape shape : shapes) { resizer.resize(shape); } } }

显然,类Resizer的实例方法resize就是接收两个参数的——第一个为Resizer类的实例对象,第二个则可能是Shape及其三个子类中的一种类的实例对象。假如 Java 的多态策略是 multiple dispatch 的,那么应当分别调用不同的三个版本的resize方法,但实际上并不是
关于为什么Java是单派发以及Common|关于为什么Java是单派发以及Common Lisp又伟大了一次的这档子事
文章图片

通过 JDK 中提供的程序javap可以看到在main方法中调用resize方法时究竟用的是类Resizer中的哪一个版本,运行命令javap -c -l -s -v Trial1,可以看到调用resize方法对应的 JVM 字节码为invokevirtual
关于为什么Java是单派发以及Common|关于为什么Java是单派发以及Common Lisp又伟大了一次的这档子事
文章图片

翻阅 JVM 规格文档可以找到对invokevirtual 指令的解释
关于为什么Java是单派发以及Common|关于为什么Java是单派发以及Common Lisp又伟大了一次的这档子事
文章图片

显然,由于在 JVM 的字节码中,invokevirtual所调用的方法的参数类型已经解析完毕——LShape表示是一个叫做Shape的类,因此在方法接收者,即类Resizer中查找的时候,也只会命中resize(Shape s)这个版本的方法。变量s的运行期类型在查找方法的时候,丝毫没有派上用场,因此 Java 的多态是 single dispatch 的。
想要依据参数的运行期类型来打印不同内容也不难,简单粗暴的办法可以选择instanceOf
abstract class AbstractResizer { public abstract void resize(Shape s); }class Resizer extends AbstractResizer { public void resize(Shape s) { if (s instanceof Circle) { System.out.println("缩放圆形"); } else if (s instanceof Rectangle) { System.out.println("缩放矩形"); } else if (s instanceof Triangle) { System.out.println("缩放三角形"); } else { System.out.println("缩放任意图形"); } } }

或者动用 Visitor 模式。
什么是 multiple dispatch? 我第一次知道 multiple dispatch 这个词语,其实就是在偶然间查找 CLOS 的相关资料时看到的。在 Common Lisp 中,定义类和方法的语法与常见的语言画风不太一样。例如,下列代码跟 Java 一样定义了四个类
(defclass shape () ())(defclass circle (shape) ())(defclass rectangle (shape) ())(defclass triangle (shape) ())(defclass abstract-resizer () ())(defclass resizer (abstract-resizer) ())(defgeneric resize (resizer shape))(defmethod resize ((resizer resizer) (shape circle)) (format t "缩放圆形~%"))(defmethod resize ((resizer resizer) (shape rectangle)) (format t "缩放矩形~%"))(defmethod resize ((resizer resizer) (shape shape)) (format t "缩放任意图形~%"))(defmethod resize ((resizer resizer) (shape triangle)) (format t "缩放三角形~%"))(let ((resizer (make-instance 'resizer)) (shapes (list (make-instance 'circle) (make-instance 'rectangle) (make-instance 'triangle)))) (dolist (shape shapes) (resize resizer shape)))

执行上述代码会调用不同版本的resize方法来打印内容
关于为什么Java是单派发以及Common|关于为什么Java是单派发以及Common Lisp又伟大了一次的这档子事
文章图片

由于defmethod支持给每一个参数都声明对应的类这一做法是在太符合直觉了,以至于我丝毫没有意识到它有一个专门的名字叫做 multiple dispatch,并且在大多数语言中是不支持的。
关于为什么Java是单派发以及Common|关于为什么Java是单派发以及Common Lisp又伟大了一次的这档子事
文章图片

后记 聪明的你应该已经发现了,在上面的 Common Lisp 代码中,其实与 Java 中的抽象类AbstractResizer对应的类abstract-resizer是完全没有必要的,defgeneric本身就是一种用来定义抽象接口的手段。
此外,在第三个版本的resize方法中,可以看到标识符shape同时作为了参数的名字和该参数所属的类的名字——没错,在 Common Lisp 中,一个符号不仅仅可以同时代表一个变量和一个函数,同时还可以兼任一个类型,它不仅仅是一门通常所说的 Lisp-2 的语言。
阅读原文

    推荐阅读