代码改变世界

面向对象语言的多分派、单分派、双重分派

2013-05-25 16:11  youxin  阅读(2624)  评论(1编辑  收藏  举报

1.     分派的概念

量被声明时的类型叫做变量的静态类型(Static Type) 又叫明显类型(Apparent Type)。变量所引用的对象的真实类型又叫做变量的实际类型(Actual Type)。

根据对象的类型而对方法进行的选择,就是分派(Dispatch)。根据分派发生的时期,可以将分派分为两种,即分派分静态分派和动态分派。

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。方法重载(Overload)就是静态分派。(所谓的:编译时多态)

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。面向对象的语言利用动态分派来实现方法置换产生的多态性。(所谓的:运行时多态)

方法重载(静态分派)

Java通过方法重载来支持静态分派。下面考察下墨子骑马的故事。

 
public abstract class Horse {

}

 

public class BlackHorse extends Horse{

}

 

public class WhiteHorse extends Horse{

}

public class Mozi
{
    public void ride(Horse h)
    {
        System.out.println("Riding a horse");
    }

    public void ride(WhiteHorse wh)
    {
        System.out.println("Riding a white horse");
    }

    public void ride(BlackHorse bh)
    {
        System.out.println("Riding a black horse");
    }

    public static void main(String[] args)
    {
        Horse wh = new WhiteHorse();

        Horse bh = new BlackHorse();

        Mozi mozi = new Mozi();

        mozi.ride(wh);
        mozi.ride(bh);
    }

    /**
     * @directed 
     */
    private Horse lnkHorse;
}

打印了两次的“Riding a horse”。 墨子发现他骑的都是马。

两次对ride()方法的调用传入的是不同的参量,也就是wh和bh。它们虽然具有不同的真实类型,但是它们的静态类型都是一样的,均是Horse类型。重载方法的分派是根据静态类型进行的。这个分派过程在编译时期就完成了

动态分派

Java通过方法的置换(Overriding)支持动态分派。

 

String s1 = "ab";
Object o = s1 +"c";
String s = "abc";
boolean b = o.equals(s);

上述代码返回了true(基础多态就不多说了)。

上面的例子中,变量s1和s的静态类型和真实类型都是String,而o的静态类型是Object,真实类型则是Object类型的一个子类型。分析: 如果上面最后一行的equals方法调用的是String类的equals方法,那么上面代码检查的就是o的值是否博字符串"abc" ,相反,如果上面的equals方法调用的是Object类的equals方法,那么检查的就是o所指的对象和s1所指的对象是不 是同一个对象. 所以问题的核心是Java编译器在编译时期并不总是知道哪一些代码会被执行,因为编译器仅仅知道对象的静态类型, 而不知道对象的真实类型,方法的调用则是根据真实类型(o的真实类型为String),而不是静态类型。变量o指向一 个类型为String的对象,这个String对象的值是"abc",这样一来,一面最后一行的equals方法调用的是String类的 equals方法,而不是Object类的equals方法.

2. 分派的类型

一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称做方法的宗量。

根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言和多分派语言。单元分派语言根据一个宗量的类型(真实类型)进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。

C++和Java以及Smaltalk都是单分派语言;多分派语言的例子包括CLOS和Cecil。按照这样的区分,C++和Java就是动态的单分派语言,因为这两种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这两种语言对重载方法的分派会考虑到方法的接收者的类型和方法所有参量的类型。

       在一个支持动态单分派的语言里面,有两个条件决定了一个请求会调用哪一个操作:一是请求的名字,二是接收者的真实类型。单分派限制了方法的选择过程,使得只有一个宗量可以被考虑到,这个宗量通常就是方法的接收者。在JAVA语言里面,如果一个操作是作用于某个类型不明的对象上面的。那么这个对象的真实类型测试仅会发生一次。这个就是动态的单分派的特征。

       一言以蔽之,JAVA语言支持静态的多分派和动态的单分派。

3.          双重分派

一个方法根据两个宗量的类型来决定执行不同的代码,这就是“双分派”或者“多重分派”。Java不支持动态的多分派。但可以通过使用设计模式,在Java语言里面实现动态的双重分派(ps:就是“伪双重分派”是由两次的单分派组成)。

方案一:类型判断

在方法里使用instanceof判断真实类型,比如(java.awt.Component的源码):

 

protected void processEvent(AWTEvent e) {
        if (e instanceof FocusEvent) {
            processFocusEvent((FocusEvent)e);

        } else if (e instanceof MouseEvent) {
            switch(e.getID()) {
              case MouseEvent.MOUSE_PRESSED:
              case MouseEvent.MOUSE_RELEASED:
              case MouseEvent.MOUSE_CLICKED:
              case MouseEvent.MOUSE_ENTERED:
              case MouseEvent.MOUSE_EXITED:
                  processMouseEvent((MouseEvent)e);
                  break;
              case MouseEvent.MOUSE_MOVED:
              case MouseEvent.MOUSE_DRAGGED:
                  processMouseMotionEvent((MouseEvent)e);
                  break;
              case MouseEvent.MOUSE_WHEEL:
                  processMouseWheelEvent((MouseWheelEvent)e);
                  break;
            }

        } else if (e instanceof KeyEvent) {
            processKeyEvent((KeyEvent)e);

        } else if (e instanceof ComponentEvent) {
            processComponentEvent((ComponentEvent)e);
        } else if (e instanceof InputMethodEvent) {
            processInputMethodEvent((InputMethodEvent)e);
        } else if (e instanceof HierarchyEvent) {
            switch (e.getID()) {
              case HierarchyEvent.HIERARCHY_CHANGED:
                  processHierarchyEvent((HierarchyEvent)e);
                  break;
              case HierarchyEvent.ANCESTOR_MOVED:
              case HierarchyEvent.ANCESTOR_RESIZED:
                  processHierarchyBoundsEvent((HierarchyEvent)e);
                  break;
            }
        }
    }

这种方法实现的双重分派都格外的冗长、复杂和容易出错,也不符合“开闭原则”

方案二:反转球

通过两次的调用来实现,比如下面剪刀石头布的游戏:
public enum Outcome { WIN, LOSE, DRAW } ///:~


interface Item {
    Outcome compete(Item it);

    Outcome eval(Paper p);

    Outcome eval(Scissors s);

    Outcome eval(Rock r);
}

class Paper implements Item {
    public Outcome compete(Item it) {
        return it.eval(this);
    }

    public Outcome eval(Paper p) {
        return DRAW;
    }

    public Outcome eval(Scissors s) {
        return WIN;
    }

    public Outcome eval(Rock r) {
        return LOSE;
    }

    public String toString() {
        return "Paper";
    }
}

class Scissors implements Item {
    public Outcome compete(Item it) {
        return it.eval(this);
    }

    public Outcome eval(Paper p) {
        return LOSE;
    }

    public Outcome eval(Scissors s) {
        return DRAW;
    }

    public Outcome eval(Rock r) {
        return WIN;
    }

    public String toString() {
        return "Scissors";
    }
}

class Rock implements Item {
    public Outcome compete(Item it) {
        return it.eval(this);
    }
    
    public Outcome eval(Paper p) {
        return WIN;
    }

    public Outcome eval(Scissors s) {
        return LOSE;
    }

    public Outcome eval(Rock r) {
        return DRAW;
    }

    public String toString() {
        return "Rock";
    }
}

public class RoShamBo1 {
    static final int SIZE = 20;
    private static Random rand = new Random(47);

    public static Item newItem() {
        switch (rand.nextInt(3)) {
        default:
        case 0:
            return new Scissors();
        case 1:
            return new Paper();
        case 2:
            return new Rock();
        }
    }

    public static void match(Item a, Item b) {
        System.out.println(a + " vs. " + b + ": " + a.compete(b));
    }

    public static void main(String[] args) {
        for (int i = 0; i < SIZE; i++)
            match(newItem(), newItem());
    }
} 

RoshamBol.match()有2个item参数,通关过Item.compete()方法开始2路分发,要判定a的类型,分发机制会在a的实际类型的compete()内部起到分发作用。compete()方法通关过eval()来为另一个类型实现第二次分发,
将自身(this)作为参数调用eval(),能够调用重载过的eval()方法,这能够保留第一次分发的类型信息,第二次分发完成时,就能知道两个Item对象的具体类型了。

这种实现也就是“访问者模式”的精华。
       这种的详细解释请看:
转自:http://architecture1.riaos.com/?p=3061963
跟多参考:http://caizhenyao.iteye.com/blog/809325