众所周知,在 Java 语言中支持基于子类型的多态,例如某百科全书中就给了一个基于Animal
及其两个子类的例子(代码经过我微微调整)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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()); } }
|
基于子类型的多态要求在程序的运行期根据参数的类型,选择不同的具体方法——例如在上述例子中,当方法letsHear
中调用了参数a
的方法talk
时,是依照变量a
在运行期的类型(第一次为Cat
,第二次为Dog
)来选择对应的talk
方法的实例的,而不是依照编译期的类型Animal
。
但在不同的语言中,在运行期查找方法时,所选择的参数的个数是不同的。对于 Java 而言,它只取方法的第一个参数(即接收者),这个策略被称为 single dispatch。
Java 的 single dispatch
要演示为什么 Java 是 single dispatch 的,必须让示例代码中的方法接收两个参数(除了方法的接收者之外再来一个参数)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| 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
方法,但实际上并不是
通过 JDK 中提供的程序javap
可以看到在main
方法中调用resize
方法时究竟用的是类Resizer
中的哪一个版本,运行命令javap -c -l -s -v Trial1
,可以看到调用resize
方法对应的 JVM 字节码为invokevirtual
翻阅 JVM 规格文档可以找到对invokevirtual 指令的解释
显然,由于在 JVM 的字节码中,invokevirtual
所调用的方法的参数类型已经解析完毕——LShape
表示是一个叫做Shape
的类,因此在方法接收者,即类Resizer
中查找的时候,也只会命中resize(Shape s)
这个版本的方法。变量s
的运行期类型在查找方法的时候,丝毫没有派上用场,因此 Java 的多态是 single dispatch 的。
想要依据参数的运行期类型来打印不同内容也不难,简单粗暴的办法可以选择instanceOf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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 一样定义了四个类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| (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
方法来打印内容
由于defmethod
支持给每一个参数都声明对应的类这一做法是在太符合直觉了,以至于我丝毫没有意识到它有一个专门的名字叫做 multiple dispatch,并且在大多数语言中是不支持的。
后记
聪明的你应该已经发现了,在上面的 Common Lisp 代码中,其实与 Java 中的抽象类AbstractResizer
对应的类abstract-resizer
是完全没有必要的,defgeneric
本身就是一种用来定义抽象接口的手段。
此外,在第三个版本的resize
方法中,可以看到标识符shape
同时作为了参数的名字和该参数所属的类的名字——没错,在 Common Lisp 中,一个符号不仅仅可以同时代表一个变量和一个函数,同时还可以兼任一个类型,它不仅仅是一门通常所说的 Lisp-2 的语言。