深入谈Java的多态机制

前言

从开始学面向对象,开始学java就在不断被灌输java几大特性:封装、继承、多态。封装有利于实现数据(状态)的隐藏,让对象的内聚性更强。继承虽说一定程度上破坏了封装,但实现了代码的复用,是多态特性实现的基础之一。多态让java的方法调用功能更加丰富,更加灵活,但带来了一定的负面作用,如可读性变差,类之间的耦合性变强。以下重点说说多态的实现原理和如何破解多态的问题!

本以为看了周志明的《深入理解java虚拟机》(第八章)之后,自己对于多态的问题都已经了若指掌,但昨天看了一篇博客之后发现依然晕头,如果大家也有我这样的自信,不妨也看看http://blog.csdn.net/thinkGhoster/article/details/2307001。下面就针对这篇博客的问题,结合周志明大神书中的例子做一个分析。

周大神的例子原文

代码清单1:

public class Dispatch{
	
	static class QQ{
	
	}
	
	static class _360{
	
	}
	
	static class Father{
		public void hardChoice(QQ arg){
			System.out.println("Father choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Father choose 360");
		}
	}
	
	static class Son extends Father{
		public void hardChoice(QQ arg){
			System.out.println("Son choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Son choose 360");
		}
	}
	
	
	public static void main(String[] args){
		Father f = new Father();
		Father s = new Son();
		
		f.hardChoice(new QQ());
		s.hardChoice(new _360());
	}
}

一个很有趣的示例,接着前几年3Q大战的背景进行调侃的。这个实例的结果应该不难分析:

Father choose QQ
Son choose 360

在编译阶段,通过静态绑定可以确定f调用的是QQ参数的方法,s调用的是_360参数的方法,就看对象的实际类型是父类的还是子类,很容易得出以上结论!

但这个代码如果改一下

代码清单2:

public class Dispatch{
	static class LiuMang{
	
	}
	
	static class QQ extends LiuMang{
	
	}
	
	static class _360 extends LiuMang{
	
	}
	
	static class Father{
		public void hardChoice(LiuMang arg){
			System.out.println("Father choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Father choose 360");
		}
	}
	
	static class Son extends Father{
		public void hardChoice(QQ arg) {
			System.out.println("Son choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Son choose 360");
		}
	}
	
	
	public static void main(String[] args){
		Father f = new Father();
		Father s = new Son();
		QQ l1 = new QQ();
		LiuMang l2 = new _360();
		f.hardChoice(l1);
		s.hardChoice(l2);
	}
}

结果又是什么呢?

Father choose QQ
Father choose QQ

是不是有点出乎意料了。怎么都在调用父类的方法,s明明是子类的对象啊!要彻底弄明白这个问题,需要从三个方面来做分析:分派、字节码指令、方法表

先说分派(Dispatch),分派也可叫绑定(binding),根据时机来分可分为静态分派和动态分派,根据宗量数量来分可分为单宗量分派和多宗量分派,组合起来有四种,静态单宗量、静态多宗量、动态单宗量、动态多宗量,这里java只使用静态多宗量和动态单宗量。

那么宗量是啥玩意呢?可理解为条件或者因子,影响分派的因素,比如方法调用者(静态类型:引用)、方法接收者(实际类型:实例)、方法签名等。

静态分派:编译时可以确定调用范围,由静态类型和方法签名共同决定,因此叫做静态多分派。

动态分派:运行时由实际的对象决定,仅仅由方法的接收者(实例)来确定,因此叫做动态单分派。


下面结合字节码指令说明一下代码清单1:



上图“1”表示静态类型的调用者,“2”表示方法签名,也就是编译器已经根据这两个宗量选择好了由哪个类(的引用)来调用哪个重载方法。这没问题,但是当运行时,“1”就会由实际类型来代替了,具体是调用哪个类(的实例)的方法,根据实例类型为准。

再看下代码清单2的字节指令:



上图“1”没改变还是静态引用的类型,2却发生很大的变化,参数列表都变成了“流氓”,为什么呢?

因为参数列表也是“静态宗量”,编译器也是根据静态类型进行判断的,编译器无法知道实参的具体类型是什么(是null也说不准啊),所以只能根据静态类型做判断了。

现在你是不是还有一个疑问,既然(s.hardChoice(l2);这条代码)方法的实际接受者是Son,为什么还要调用父类的方法呢?

其实也不是调用父类的方法了,而是子类Son的方法,因为Son继承了Father的所有的虚方法(概念等会讲),就会拥有这个方法,因为没有覆盖这个方法,所以内容就跟父类方法完全一样了,看起来是不是有点晕,没关系,看一下虚方法表(伪表)就明白了。

Father methodTable[0]=hardChoice(com.sia.send.test.Dispatch$LiuMang)
Father methodTable[1]=hardChoice(com.sia.send.test.Dispatch$_360)
Father methodTable[2]=wait(long,int)
Father methodTable[3]=wait(long)
Father methodTable[4]=wait()
Father methodTable[5]=equals(java.lang.Object)
Father methodTable[6]=toString()
Father methodTable[7]=hashCode()
Father methodTable[8]=getClass()
Father methodTable[9]=notify()
Father methodTable[10]=notifyAll()
Son methodTable[0]=hardChoice(com.sia.send.test.Dispatch$LiuMang)
Son methodTable[1]=hardChoice(com.sia.send.test.Dispatch$_360)
Son methodTable[2]=hardChoice(com.sia.send.test.Dispatch$QQ)
Son methodTable[3]=wait(long,int)
Son methodTable[4]=wait(long)
Son methodTable[5]=wait()
Son methodTable[6]=equals(java.lang.Object)
Son methodTable[7]=toString()
Son methodTable[8]=hashCode()
Son methodTable[9]=getClass()
Son methodTable[10]=notify()
Son methodTable[11]=notifyAll()

父类有两个hardChoice,但子类由三个,有两个于父类一毛一样,子类源码中我们只定义两个hardChioce,有一个继承了父类的,那么
Son methodTable[0]=hardChoice(com.sia.send.test.Dispatch$LiuMang)
这个一定是父类继承过来的了,当然与父类的行为一模一样,所以即使在动态分派时调用该方法,看起来也像是调的父类的方法。
注:真正的虚方法表一般也会把父子类相同方法的 索引一一对应,这样在进行动态分派时可以只改变对象的接收者而不用再表中重新查询一遍了,提高效率。


行文至此,几乎所有问题都说清楚了,还有一个问题,什么是虚方法,什么是非虚方法。

从行为上来说:

非虚方法不会产生动态分派,具有“编译时可知,运行时不改变”的特点。包括静态方法(invokestatic),构造方法(invokespecial),私有方法(invokespecial)和父类方法(invokespecial),还包括final方法(invokevirtual),因为final方法不可被覆盖,因此也不存在运行时多态(编译时即可唯一确定),有人建议方法使用final修饰可以提高调用效率50%,就是这个原因,无需到子类的虚方法表中查询是否覆盖了本方法。

所谓的虚方法,就是使用invokevirtual指令调用的方法,具体就是非private非final实例方法,这种调用可能产生多态,调用效率也最低,但正是由于这个功能才使得java具有丰富的功能和灵活性。


最后再来段代码清单3,由读者去想结果:

public class Dispatch{
	static class LiuMang{
	
	}
	
	static class QQ extends LiuMang{
	
	}
	
	static class _360 extends LiuMang{
	
	}
	
	static class Father{
		public void hardChoice(Object arg) {
			System.out.println("Father choose Object");
		}

		public void hardChoice(LiuMang arg) {
			System.out.println("Father choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Father choose 360");
		}
	}
	
	static class Son extends Father{
		public void hardChoice(QQ arg) {
			System.out.println("Son choose QQ");
		}
		
		public void hardChoice(_360 arg){
			System.out.println("Son choose 360");
		}
	}
	
	
	public static void main(String[] args){
		Father f = new Father();
		Father s = new Son();
		QQ l1 = new QQ();
		_360 l2 = new _360();
		f.hardChoice(l2);
		s.hardChoice(l1);
	}
}







  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值