[200131]Java多态 - Thinking in Java学习日志

什么是多态?

面向对象的程序设计语言中,抽象、继承、多态是三大基本特征。

从向上转型说起

我们知道,Java中可以进行向上转型(即把一个导出类的引用赋给其基类的引用),向上转型其实是一个不对等的过程:导出类的接口是基类接口的超集——基类有的接口导出类一定有,导出类有的接口基类不一定有。这就导致向上转型造成了某种意义上的损失:不能够调用导出类中特有的接口。
那么共有的接口呢?这就涉及到了多态这个概念,请看以下代码:

package com.phycanva.demonstrate;

public class Base_poly {
	public void out () {System.out.println("基类方法");}
	
	public static void main(String[] args) {
		Base_poly base = new Sub_poly();
		base.out();

	}

}

class Sub_poly extends Base_poly{
	 public void out() {System.out.println("导出类方法");}
}

在这个例子中,基类和导出类有一个同名方法,很明显通过继承,导出类方法覆盖掉了基类方法。但是当我们进行向上转型的时候,事情就有点不对劲儿了:new和导出类的构造器返回一个导出类对象的引用,赋给了一个基类引用。我们知道,如果我们正儿八经地构造一个基类对象,调用的肯定是基类定义的方法。那这个又像基类又像导出类的玩意儿,调用的方法是基类的还是导出类的?
运行一看便知:

导出类方法

事实证明,调用了导出类的方法。即使引用类型跟了基类,它最终还是认这个构造器的,不难猜想在引用所指之处有一处内存是用来标识它真正的身份,让它在调用方法的时候也不至于迷失自我。
这时候有人就会说:折腾个什么劲儿啊?直接老老实实构造一个导出类不就行了?这个向上转型有个啥意义?
孰不知,正是向上转型,成就了多态这个重要特性

多态实现了代码复用

众所周知,在OOP语言中,一个基类可以被多个导出类所继承,也就是说:多种导出类都可以向上转型,共用同一种基类的引用! 这个共用的操作对于解放程序员的生产力十分有意义,请看以下代码:

package com.phycanva.demonstrate;

public class Human {
	public void say(){System.out.println("我是人");}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		saySomething(new Man());
		saySomething(new Woman());
	}
	public static void saySomething(Human h) {
		h.say();
	}
}
class Man extends Human{
	public void say() {System.out.println("我是男人");}
}
class Woman extends Human{
	public void say() {System.out.println("我是女人");}
}

·运行结果可想而知:

我是男人
我是女人

在调用saySomething方法的时候,看似传Man的引用和一个Woman的引用都可以,事实上saySomething方法根据定义,只能接收Human类型的引用:这意味着自动进行了向上转型过程,实际传入的是Human类型的引用。
上面的代码已经体现了多态带给我们的福利:我们无需去费心费力地为每一个继承于Human的类的对象重载一个函数(例如 void saySomething(Man m) 和 void saySomething(Woman w)),而是统统当成基类对待,给基类写方法。

其实这是一个很明朗的逻辑:
既然你是Human,你肯定可以调用say这个方法。我只管把你当Human看,至于怎么say,就具体类具体分析吧!
类似地:
既然你是基类继承下来的,你肯定有基类的接口,我只管把你当基类看,具体要怎么实现,你这个类内部来决定吧!

多态的魅力实际来自于它的“大度”,它舍弃了“繁文缛节”,不会追究类的具体类型,它只需要知道你是某个基类继承下来的,具体的工作便交给类自己去实现

这种“大度”显然给广大程序员带来了便利:当我们为基类新增一个导出类的时候,只需要让导出类“独善其身”,把公共的接口代码写好,然后管理好新增的接口就好。大可不必东奔西走去重载方法以维护旧有接口。

正如书中所说

多态是一项让程序员“将改变的事物与未变的事物分离开来”的重要技术

相关概念介绍

绑定

将一个方法调用同一个方法主体关联起来被称作绑定。 —— Bruce Eckel《Thinking in Java》

绑定分为两种:

  • 前期绑定
  • 后期绑定(动态绑定、运行时绑定)

所谓“前期绑定”,就是在程序执行之前进行绑定,这个工作往往被交给编译器和连接程序之类的来做。
例如C语言只有前期绑定:调用关系在编译期间就已经确定。

所谓“后期绑定”,就是在运行时根据对象的类型进行绑定,这就需要在对象中保存某种“类型标识”。
Java除了 static方法和 final方法(包括 private方法),其他所有方法都是后期绑定,这是自动发生的。

我们之所以能够在进行向上转型后,仍然调用到导出类方法,就是托了后期绑定的福。

向下转型

向上转型的过程中,我们丢失了信息、获得了安全保障
向下转型的过程中,我们获得了信息、丢失了安全保障

当向下转型发生的时候,就不可避免地产生一些安全隐患:贸然转型到一个错误地类型、并向其发出无法接受的信息时,这是十分危险的。所以Java派出了一位助手来检查向下转型是否安全:运行时类型识别(RTTI)。当我们一顿操作猛如虎、进行酣畅淋漓的向下转型时,RTTI总能及时发现错误(运行时才能发现,感觉还是不太及时),给我们泼一盆凉水、并向你扔出一个ClassCastException(类转型异常)。

缺陷

静态方法没有多态性

多态通过动态绑定来实现,一“动”一“静”必然存在着矛盾之处,前面也提到过,static方法不是后期绑定的,故其不具有多态性。静态方法是与类,而不是与单个的对象相关联的

域没有多态性

package com.phycanva.demonstrate;

public class Human {
	public int data =  1;
	public void say(){System.out.println("我是人");}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		saySomething(new Man());
		saySomething(new Woman());
	}
	public static void saySomething(Human h) {
		h.say();
		System.out.println(h.data);
	}
}
class Man extends Human{
	public int data = 2;
	public void say() {System.out.println("我是男人");}
}
class Woman extends Human{
	public int data = 3;
	public void say() {System.out.println("我是女人");}
}

以上代码在原有的基础上,在所有类中增加了int成员
以下是运行结果

我是男人
1
我是女人
1

当向上转型发生时,任何域访问的操作都是交给编译器一手操办,这就意味着域没有多态性
可见这时候就没有什么“动态绑定了”,基类的引用访问基类的域(真是实诚的编译器啊~)

另:java会为基类和导出类中同名的域分配不同的储存空间:即在本例中,Man包含两个称为data的域。在导出类内部,为了得到基类的同名域,必须进行显式调用(例如 human.data)

后记

由于仍处于学习阶段,文章中的内容很可能不够准确也不够全面。文中若有错误出现,敬请指出,一定虚心改正;有需要补充修改的地方,也请各位不吝赐教,十分乐意与大家交流学习。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值