乍听之下,不无道理;仔细揣摩,胡说八道

0%

为什么管Java叫单派发以及什么是多派发

众所周知,在 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
// 演示 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方法,但实际上并不是

通过 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 的语言。

Liutos wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
你的一点心意,我的十分动力。