单分派和多分派

来源:互联网 发布:风冷 直冷 知乎 编辑:程序博客网 时间:2024/06/05 09:13

在学习访问者模式时,看到了single dispatch(单分派),double dispatch(双分派)和multi dispatch(多分派)的概念,这里转载一篇网上讲的关于单分派和多分派的文章:



一:分派的概念 
变量被声明时的类型叫做变量的静态类型,或叫做明显类型,而变量所引用的对象的真实类型叫做实际类型,这种根据对象的类型而对方法进行的选择,就是分派.分派是面向对象的语言所提供的关键特性之一,根据分派发生的时期 ,分为静态分派和动态分派. 
静态分派发生在编译时期,分派根据静态类型信息发生,如方法重载就是静态分派 
动态分派发生在运行时期,动态分派动态地转换掉某个方法,面向对象的语言利用动态分派来实现方法转换产生多态性. 
(a)方法重载----墨子骑马 

Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2. public abstract class Horse {  
  3.   
  4. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class BlackHorse extends Horse{  
  4.   
  5. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class WhiteHorse extends Horse{  
  4.   
  5. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class Mozi {  
  4. private Horse lnkHorse;  
  5. public void ride(Horse h) {  
  6. System.out.println("Riding a horse");  
  7. }  
  8. public void ride(WhiteHorse wh) {  
  9. System.out.println("Riding a white horse");  
  10. }  
  11. public void ride(BlackHorse bh) {  
  12. System.out.println("Riding a black horse");  
  13. }  
  14. public static void main(String[] args) {  
  15. Horse wh = new WhiteHorse();  
  16. Horse bh = new BlackHorse();  
  17. Mozi mozi = new Mozi();  
  18. mozi.ride(wh);  
  19. mozi.ride(bh);  
  20. }  
  21. }  

显然Mozi类的ride方法是由三个方法重载而成的,这三个方法分别接收马,白马,黑马。两次调用ride方法输出的都是Riding a horse.想下,两次对ride方法的调用传入的是不同的参量,也就是wh和bh,它们虽然具有不同的真实类型,但是它们的明显类型都是House类型。重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成 了.这就是静态分派的例子. 


(b)动态分派--Java通过方法的转换(Overriding)支持动态分派 
请看下面的代码: 
Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class DongTaiFenPai {  
  4. public static void main(String[] args){  
  5. String s1 = "ab";  
  6. Object o = s1 "c";  
  7. String s = "abc";  
  8. boolean b = o.equals(s);//o的真实类型为String  
  9. System.out.println(b);  
  10. }  
  11. }  


上面的例子中,变量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方法. 

二:分派的类型 
一个方法所属的对象叫做方法的接收者,方法的接收者与方法的参量统称为方法的宗量.根据分派可以基于多少种宗量,可以将面向对象的语言分为单分派语言和多分派语言.单分派语言根据一个宗量的类型进行对方法的选择,多分派语言根据多于一个宗量的类型对方法进行选择.Java是单分派语言,因为它动态分派仅仅会考虑到方法的接收者类型,同时又是静态的多分派语言,因为它对重载方法的分派会考虑到方法的接收者类型以及方法的所有参量的类型. 在一个支持动态单分派的语言里(如Java),有两个条件决定了一个请求会调用哪一个操作,一是请求的名字,二是接收者的真实类型,单分派限制了方法的选择过程,使得只有一个宗量可以被考虑到,这个宗量通常就是方法的接收者。总之,Java语言支持静态的多分派和动态的单会派. 

三:一个静态分派和动态分派的例子 
下面的例子说明了在Java语言中静态多分派和动态单分派是如何发生的: 
Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. import java.awt.Canvas;  
  4.   
  5. public class Point {  
  6. private int x;  
  7. private int y;  
  8. public Point() {  
  9. }  
  10. public void draw(Canvas c) {  
  11. // write you code here  
  12. }  
  13. public void translate(int d) {  
  14. x = d;  
  15. y = d;  
  16. }  
  17. public void translate(int dx, int dy) {  
  18. x = dx;  
  19. y = dy;  
  20. }  
  21. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. import java.awt.Canvas;  
  4. import java.awt.Color;  
  5.   
  6. public class ColorPoint extends Point {  
  7. private Color c;  
  8. public ColorPoint() {  
  9. }  
  10. public void draw(Canvas C) {  
  11. // Color point code  
  12. }  
  13. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. import java.awt.Canvas;  
  4.   
  5. public class Client {  
  6. private static Point p;  
  7. private static Point pc;  
  8. public static void main(String[] args) {  
  9. p = new Point();  
  10. pc = new ColorPoint();  
  11. // static multi-dispatch  
  12. p.translate(5); // one int version  
  13. p.translate(12); // two int version  
  14. // dynamic uni-dispatch  
  15. Canvas aCanvas = new Canvas();  
  16. p.draw(aCanvas); // Point.draw()  
  17. pc.draw(aCanvas); // ColorPoint.draw();  
  18. }  
  19. }  


从Client可以看出,由于方法的重载,对Point对象的两种translate方法的调用是典型的静态多分派,而对子类以及父类的draw方法调用则是典型的动态单分派. 

四:双重分派 
一个方法根据两个宗量的类型来决定执行不同的代码,这就是"双分派"或者"双重分派",Java语言不支持动态的多分派,但是通过设计模式也可以在Java语言里实现动态的双重分派.双重分派是多重分派的一种,双重分派是由连续的两次单分派组成的。怎么样在Java语言中实现动态的双重分派呢?下面看下用"返传球"实现双重分派: 
(a)返传球 

仔细想想,既然Java语言支持动态的单分派,那么为什么不可以通过两次方法调用来达到两次分派的目的呢?假设有两个类West和East,这两个类分别有两个方法,叫做goWest和goEast,现在west对象首先调用east对象的goEast方法并将自己传入,在east对象被调用时,立即根据传入的参量知道调用者是谁,于是反过来调用调用者对象的goWest方法,通过两次调用将程序控制权轮番交给两个对象,这样就出现了两次方法调用,程序的控制权被两个对象像传球一样,首先由west对象传给了east对象,然后又被返传给了west对象。 


(b)方法的置换 
仅仅返传了一下球,并不能解决双重分派的问题,关键是怎么利用这两次调用,以及Java语言的动态单分派功能,使得在这种传球过程中能够触发两次(动态)单分派. 动态单分派在Java语言中是在子类型将父类型的方法置换掉时发生的,换言之,West和East都必须分别置身于自己的类型等级结构中.于是在这个系统里,有一个客户端,以及两个等级结构---West等级结构和East等级结构,在West等级结构中,由West定义出公共的类型,并有两个具体子类实现了抽象类的接口,而East等级结构也与此相像. 
Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public abstract class East {  
  4. public abstract void goEast(West west);  
  5. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class SubEast1 extends East {  
  4. public void goEast(West west) {  
  5. west.goWest1(this);  
  6. }  
  7.   
  8. public String myName1() {  
  9. return "SubEast1";  
  10. }  
  11. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class SubEast2 extends East {  
  4. public void goEast(West west) {  
  5. west.goWest2(this);  
  6. }  
  7.   
  8. public String myName2() {  
  9. return "SubEast2";  
  10. }  
  11. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public abstract class West {  
  4. public abstract void goWest1(SubEast1 east);  
  5. public abstract void goWest2(SubEast2 east);  
  6. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class SubWest1 extends West {  
  4. public void goWest1(SubEast1 east) {  
  5. System.out.println("SubWest1 " east.myName1());  
  6. }  
  7. public void goWest2(SubEast2 east) {  
  8. System.out.println("SubWest1 " east.myName2());  
  9. }  
  10. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. public class SubWest2 extends West {  
  4. public void goWest1(SubEast1 east) {  
  5. System.out.println("SubWest2 " east.myName1());  
  6. }  
  7. public void goWest2(SubEast2 east) {  
  8. System.out.println("SubWest2 " east.myName2());  
  9. }  
  10. }  


Java代码  收藏代码
  1. package cai.milenfan.basic.test;  
  2.   
  3. import java.awt.Canvas;  
  4.   
  5. public class Client {  
  6. private static East east;  
  7. private static West west;  
  8. public static void main(String[] args) {  
  9. // combination 1  
  10. east = new SubEast1();  
  11. west = new SubWest1();  
  12.   
  13. east.goEast(west);  
  14.   
  15. // combination 2  
  16. east = new SubEast1();  
  17. west = new SubWest2();  
  18.   
  19. east.goEast(west);  
  20.   
  21. }  
  22. }  
  23.   
  24. output:  
  25. SubWest1 SubEast1  
  26. SubWest2 SubEast1  

系统运行时,会首先创建SubWest1和SubEast1对象,然后客户端调用SubEast1的goEast()方法,并将SubWest1对象传入(由于SubEast1对象置换了其超类East的goEast方法,因此这个时候就发生了一次动态单分派)。当SubEast1对象接到调用时,会从参数中得到SubWest1对象,所以它就立即调用这个对象的goWest1方法,并将自己传入(由于SubEast1对象有权选择调用哪一个对象,因此在此时又进行一次动态的方法分派) 由output知道,两个名字一个来自East等级结构,一个来自West等级结构,困此它们的组合式是动态决定的,这就是动态双重分派的实现机制. 最后需要指出的是,首先East和West等级结构的大小是彼此独立的,虽然在上面的例子中两者都包含了两个具体成员 ,但这只是巧合。其次West要访问的East等级结构中的节点数目应当与West对象所配备的方法的数目相等,这是必然的. 这种返传球的设计就是访问者模式的精华

0 0