關於為什麼Java是單派發以及Common Lisp又偉大了一次的這檔子事

Liutos發表於2021-10-17

眾所周知,在 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());
  }
}

基於子型別的多型要求在程式的執行期根據引數的型別,選擇不同的具體方法——例如在上述例子中,當方法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方法,但實際上並不是

通過 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

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方法來列印內容

由於defmethod支援給每一個引數都宣告對應的類這一做法是在太符合直覺了,以至於我絲毫沒有意識到它有一個專門的名字叫做 multiple dispatch,並且在大多數語言中是不支援的。

後記

聰明的你應該已經發現了,在上面的 Common Lisp 程式碼中,其實與 Java 中的抽象類AbstractResizer對應的類abstract-resizer是完全沒有必要的,defgeneric本身就是一種用來定義抽象介面的手段。

此外,在第三個版本的resize方法中,可以看到識別符號shape同時作為了引數的名字和該引數所屬的類的名字——沒錯,在 Common Lisp 中,一個符號不僅僅可以同時代表一個變數和一個函式,同時還可以兼任一個型別,它不僅僅是一門通常所說的 Lisp-2 的語言。

閱讀原文

相關文章