静态分派以及多分派
静态分派就是按照变量的静态类型进行分派,从而确定方法的执行版本,静态分派在编译时期就可以确定方法的版本。而静态分派最典型的应用就是方法重载,考虑下面一段程序。
public class Main {
public void test(String string){
System.out.println("string");
}
public void test(Integer integer){
System.out.println("integer");
}
public static void main(String[] args) {
String string = "1";
Integer integer = 1;
Main main = new Main();
main.test(integer);
main.test(string);
}
}
相信运行结果不需要LZ给出了,会依次打印integer和string,对于test方法,会根据静态类型决定方法版本,而所判断的依据就是,在main类型确定之后,依据test方法的参数类型和参数数量,我们就可以唯一的确定一个重载方法的版本。比如上面的例子,我们确定完main的类型之后,就可以根据test方法是一个参数,并且这个参数是Integer类型还是String类型,就可以确定到底调用哪个重载方法了。
可以看到,在静态分派判断的时候,我们根据多个判断依据(即参数类型和个数)判断出了方法的版本,那么这个就是多分派的概念,因为我们有一个以上的考量标准,也可以称为宗量。所以JAVA是静态多分派的语言。
动态分派以及单分派:
对于动态分派,与静态相反,它不是在编译期确定的方法版本,而是在运行时才能确定。而动态分派最典型的应用就是多态的特性,考虑下面一段程序。
动态分派以及单分派:
对于动态分派,与静态相反,它不是在编译期确定的方法版本,而是在运行时才能确定。而动态分派最典型的应用就是多态的特性,考虑下面一段程序。
package com.visitor1;
interface Person{
void test();
}
class Man implements Person{
public void test(){
System.out.println("男人");
}
}
class Woman implements Person{
public void test(){
System.out.println("女人");
}
}
public class Main {
public static void main(String[] args) {
Person man = new Man();
Person woman = new Woman();
man.test();
woman.test();
}
}
这段程序输出结果为依次打印男人和女人,这应该是所有人都预料到的,然而现在的test方法版本,就无法根据man和woman的静态类型去判断了,他们的静态类型都是Person接口,根本无从判断。
显然,产生的输出结果,就是因为test方法的版本是在运行时判断的,这就是动态分派。
动态分派判断的方法是在运行时获取到man和woman的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念,这时我们的考量标准只有一个宗量,即变量的实际引用类型。相应的,这说明JAVA是动态单分派的语言。
访问者模式中的伪动态双分派:
上面LZ已经简单的介绍了JAVA语言中的静态多分派和动态单分派,更详细的解释各位可以在其它文献和文章中寻找,这里我们谈谈访问者模式的分派方式。
标题上已经注明了,访问者模式中使用的是伪动态双分派,之所以加了一个“伪”字,是因为一个模式当然不可能更改语言的特性,所以JAVA是动态单分派的语言这点毋庸置疑,而访问者模式只是利用了一些手段达到了看似双分派的效果。
对于动态双分派这个词语,初次接触的读友或许比较迷惑,LZ先给大家一个初步的解释,动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。
我们来看上面例子当中账本类中的accept方法的调用。
for (Bill bill : billList) {
bill.accept(viewer);
}
为什么要强调是accept方法的动作而不是方法的版本,是因为accept方法的版本只需要一次动态分派就可以确定,但是它所产生的动作却需要两次动态分派才能确定。
我们来看下这个accept方法的调用过程,LZ还是分步骤给各位解释。
1、当调用accept方法时,根据bill的实际类型决定是调用ConsumeBill还是IncomeBill的accept方法。
2、这时accept方法的版本已经确定,假如是ConsumeBill,它的accept方法是调用下面这行代码。
此时的this是ConsumeBill类型,所以对应于AccountBookViewer接口的view(ConsumeBill bill)方法,此时需要再根据viewer的实际类型确定view方法的版本,如此一来,就完成了动态双分派的过程。
以上的过程就是通过两次动态双分派,第一次对accept方法进行动态分派,第二次对view(类图中的visit方法)方法进行动态分派,从而达到了根据两个实际类型确定一个方法的行为的效果。
而原本我们的做法,通常是传入一个接口,直接使用该接口的方法,此为动态单分派,就像策略模式一样。在这里,show方法传入的viewer接口并不是直接调用自己的view方法,而是通过bill的实际类型先动态分派一次,然后在分派后确定的方法版本里再进行自己的动态分派。
在此之外,还需要再解释一点,在上面第2步,确定view(ConsumeBill bill)方法是静态分派决定的,所以这个并不在此次动态双分派的范畴内,而且静态分派是在编译期就完成的,也就是说,在上述第1步之前就已经完成了对view(ConsumeBill bill)方法版本的选取。况且把静态分派算在内的话,由于静态分派是多分派,这里就不能叫双分派了,应该叫动态多分派,这显然是不成立的。所以view(ConsumeBill bill)方法的静态分派与访问者模式的动态双分派并没有任何关系。
而且退一步讲,我们完全可以将AccountBookViewer接口中的两个view方法取不同的名字,这样也就完全避免了方法版本确定中静态分派参与的嫌疑,而且这完全不影响访问者模式的效果,可以清楚的看到,标准类图中也是这么建议的。这里LZ写成一样的名字,只是为了方便和更加清晰,而且在只有两个方法的时候这么做也并无不可,但是如果方法多的时候,强烈建议不要取一样的名字,由于静态分派的重载版本往往不是唯一的,所以重载版本过多会造成一定的干扰。
LZ的例子只是为了更加清晰的展示访问者模式,在实际应用中,还是强烈建议各位使用不同的方法名称去命名各个元素的访问方法。
在这里LZ解释静态分派不参与双分派的原因,是因为看到不少文章都多多少少在暗示访问者模式的双分派与view(this)的静态分派相关,这个观点恕LZ不能苟同,动态双分派说到底还是动态分派,是在运行时发生的,它与静态分派有着本质上的区别,不可以说一次动态分派加一次静态分派就是动态双分派,而且访问者模式的双分派本身也是另有所指。
你可能会说,this的类型是动态确定的,这么算下来不也是根据AccountBookViewer的实际类型和Bill的实际类型做了一次动态双分派吗?
答案是当然不能这么算,this的类型可不是动态确定的,你写在哪个类当中,它的静态类型就是哪个类,这是在编译期就确定的,不确定的是它的实际类型,请各位区分开这一点。
如果各位只是为了暂时理解访问者模式的双分派,这样理解倒也不是不可,以后随着理解的加深,会渐渐更加清晰。不过如果你这么理解访问者模式的双分派,一定要搞清楚一点,那就是this的类型是在编译期就可以确定的,而不是在运行时动态确定的,它并不是真正的动态绑定,而且说到底它始终是一个参数类型,参与分派的是它的静态类型,依旧是静态分派的范畴。总之怎么理解是自己的事,LZ只是试图点破一下这里面的分派关系。
上面的分析算是一个铺垫,至于后面对于静态分派的分析,各位可以当做一个讨论,也可以在文章下方发表评论参与进来,LZ不胜欢迎。
接下来我们还有最后一件事没完成了,那就是前面提到的优点的后两点的体现,因为在前面的例子当中没有用到,所以可能会理解起来会有点困难,这里LZ把上面的例子优化一下,让各位体验一下优点中的后两点是多么强大。
我们先来考虑一个问题,假设我们上面的例子当中再添加一个财务主管,而财务主管不管你是支出还是收入,都要详细的查看你的单子的项目以及金额,简单点说就是财务主管类的两个view方法的代码是一样的。
你可能会说,这好办啊,我们可以复制一下嘛。是的,这当然是一个办法,不过相信对代码有要求的程序猿们一定不喜欢他们的代码里出现一堆的重复代码。而且假设有很多人的访问方式和财务主管一样,对收入和支出的操作一样,那得复制多少代码。
解决方案就是我们可以将元素提炼出层次结构,针对层次结构提供操作的方法,这样就实现了优点当中最后两点提到的针对层次定义操作以及跨越层次定义操作。