面向对象 浅谈多态



    1,多态产生的原因

什么是多态了?一种事物有多种状态。这是对多态的通用解释,在Java中的多态又是如何的了?要了解多态,先从它产生的原因说起。

相信看到文章的你清楚的明白 javac 和 java 这两个命令了,一个是编译java源文件,一个是运行编译后的文件。在解释清楚产生的原因前,我先创建几个类:Animal(动物) Cat(猫咪) Dog(汪星人)。其中Cat Dog 都继承自 Animal。

Animal  foo = new Animal();

Cat niki = new Cat();

这是我们熟悉的创建对象的方式,上面提到的:java有编译和运行两阶段。对于上述语句在表达式左边的类型(Animal)是 foo的编译期类型;在运行阶段,JVM会检查表达式右边的类型,也是Animal。对于第二句也是同样的道理。

通俗的说法:你要一个动物(编译) ,我给你一个动物(运行(具体是什么不得而知,可以肯定它一定要是动物));你要一只小猫(编译),我给你一只小猫(运行)。逻辑上是没问题的。现在情况是这样的,我要一个动物,你给我一只小猫,这个逻辑也行的通,但是它隐含的条件是猫的确是动物,你给我一个玩具猫那可不行,从分类上讲玩具猫是玩具。

通过上述表达,我们再来看看java中多态产生的原因:具有继承关系的类之间,对于同一对象的引用,编译期间和运行期间的类型不相同。代码描述如下:

Animal foo = new Cat();

2,向上转型

对于 

Animal foo = new Cat();

编译期间,编译器会检查 foo 这个引用的类型:为Animal。但是在实际运行时,我们new 的是一个Cat类型的实例对象,那么foo引用指向的是一个Cat 类型的对象。为了解决这种情况,JVM会将这个Cat类型提升为Animal类型,这就是向上转型。

上述情况可简述:子类对象赋给父类引用,编译器会完成向上转型。向上转型是建立在继承树上的,之所以称为向上转型,是因为引用类型在继承树是由下往上移动的。

 

在继续阅读之前,我们需要了解通常的说法,以免有的读者感到困惑。

静态方法--类方法,用static 关键字修饰,专属于类,不依赖于对象。

非静态方法--实例(对象)方法,无static关键字修饰,依赖于实际创建的对象。

下面是代码:

class Animal  //定义一个Animal类
{
public void eat(){  //定义了eat()和cry()两个实例方法
System.out.println("they eat what they eat");
}
public void cry(){
System.out.println("animals may cry!");
}
}
class Cat extends Animal  //定义一个Cat类继承自Animal
{
public void eat(){  //覆盖了父类的 eat() 和 cry()方法
System.out.println("吃鱼");
}
public void cry(){
System.out.println("喵!");
}
public void name(){  //定义自己特有方法 name()
System.out.println("niki");
}
}
class Dog extends Animal  //定义一个Dog类,继承自Animal
{
public void eat(){   //覆盖了父类的 eat() 和 cry()方法
System.out.println("啃骨头");
}
public void cry(){
System.out.println("汪汪!");
}
public void name (){   //定义自己特有的方法 name()
System.out.println("coco");
}
}
public class Demo
{
public static void main(String args[]){
Animal foo = new Animal();//引用类型与对象类型一致
Animal cat = new Cat();  //引用类型与对象类型不一致
Animal dog = new Dog();  //引用类型与对象类型不一致
/*Dog dog = new Cat();
显然你不能这么干,从实际生活情况考虑:我要一只狗,你给我一只猫。
从语法上说,它们的类型不兼容,因为它们之间无继承关系。当然你也
可以让猫这个类继承狗来完成向上转型,但是这是很奇怪的一个逻辑,
语法上是能通过的。
*/
foo.eat();   
foo.cry();
cat.eat();
cat.cry();
//cat.name();  无法通过编译
dog.eat();
dog.cry();
//dog.name();  无法通过编译
}
}




---------- 运行java程序 ----------

they eat what they eat

animals may cry!

吃鱼

喵!

啃骨头

汪汪!

------------------------------------

先来说说无法通过编译的两句,在了解这以后对多态就有了进一步的了解。Animal dog = new Dog();在编译时,编译器会检查dog这个引用的类型:Animal;编译阶段编译器会认为它就是一个Animal类型,再来看我们没有通过编译语句:dog.name();编译器会去查找Animal这个类中是否有name这个方法。从Animal类的定义中,我们并没有看到name()方法,也就不指望编译器能通过了。

这就告诉我们,多态的局限性,你只能使用父类中有的方法,子类特有的方法无法调用,因为编译阶段,子类对象还没有产生,你无法预知子类对象有何种不同的方法。

解决了上述问题我们再来看输出结果为什么是这样的:cat.eat() ,这句能通过编译,cat引用去查找Animal类,发现有eat这个方法,但是输出的结果告诉我们,它实际调用的是Cat中的eat()方法,原因很简单。Animal cat  = new Cat();这一句我们一分为二的看,左边和右边,左边类型是编译期类型,右边是运行期引用实际指向的对象。在编译期,cat引用是Animal类型,查找到eat()方法,运行时,创建的实际是Cat对象,就如同开始提到的实例(非静态)方法,依靠的是对象本身,这也就解释了为何输出的是上述结果。

这只是对多态的一个初步认识,针对的是实例(非静态)方法。现在总结如下:

在编译时期:参阅引用类型变量所属的类中是否有调用的方法。如果有,编译通过,如果没有编译失败。
在运行时期:参阅对象所属的类中是否有调用的方法

这里很明显我们并没有提到:成员变量 和 静态方法。需记住这一下两点
1)成员变量:编译和运行,都参考左边(引用类型变量所属的类)

2)静态成员函数的特点:编译和运行,都参考左边 

3,向下转型

   有向上转型,就有向下转型。再来看看我们上面的代码片段:

 Animal cat = new Cat();

 //cat.name() 无法通过编译

现在的情况又改变了:我们知道在编译阶段cat引用的类型是Animal,如果想要调用Cat类中特有的name()方法,该如何实现了?这里就用到了向下转型了。你可以强制性的这样做:

Cat anotherCat = (Cat)cat;

anotherCat.name(); 

我们将cat引用的类型从Animal强制转换为Cat,并将引用赋给另外一个引用。最后去调用name()方法。但是这里有一个问题,这里能做强制转换,是因为我们明确的知道我们new 的是Cat对象,在做向下转型时也就很明确的知道转成Cat类型,虽然Dog类也是Animal的子类。但是这种做法并不明,有时候并不确定它们在继承树中是否有关系,可以看看下面这段代码:

class Animal
{
public void eat(){
System.out.println("they are what they eat");
}
}
class Dog extends Animal
{
public void eat(){
System.out.println("fash");
}
}
class Cat extends Animal
{
public void eat(){
System.out.println("bone");
}
}
class Toy
{
}
class Boat
{
}
public class Demo2
{
public static void main(String args[]){
fun(new Animal());
fun(new Cat());
fun(new Dog());
fun(new Toy());
fun(new Boat());
}
public static void fun(Object obj){
if(obj != null && obj instanceof Animal){
                 Animal foo = (Animal)obj;
 	foo.eat();
}
else
System.out.println("not animal!");
}
}




---------- 运行java程序 ----------

they are what they eat

bone

fash

not animal!

not animal!

------------------------------------

我们以其中高亮的部分来说明:

fun(new Animal());

当调用这个函数时,我们传入的是一个Animal类型的引用,但是fun函数在编写时要求接受的参数类型是Object,由于Object是所有类的父类,Animal对象的引用自动向上转型为Object,完成参数的传递。此时形式参数obj 的类型是Object指向的是Animal类型的实例对象:

                 Object obj = new Animal();

参数传递的过程可以理解成这样。再来看:

                 if(obj != null && obj instanceof Animal)

首先要保证obj指向了某个对象,后面的 obj  instanceof Animal 可以表述为

   obj (指向的实例对象)是 Animal 这个类的实例对象吗?instance(实例)of(是谁的) 这就是 instanceof 关键字的组成,a instanceof  X :前者是后者的一个实例对象吗?通过判断是obj 是Animal 的实例对象后,可以明确的进行强制类型转换了。至此,也就完成了向下转型。

向下转型比向上转型要麻烦。其实从开头的类比中我们也能理解:

我要一直动物,给我一只猫一条狗或一头猪,都没问题,这就是向上转型;如果我要的是一条蛇,你给的了一个放在黑盒子里的动物(我并不清楚是什么动物),这就无法确定你到底给我什么动物了,这就是向下转型存在的问题。

对于多态,其实我们一直操作的是同一个对象,变化的只是引用类型,不论是向上转型还是向下转型,我们都需要明确的知道它们运行的各个阶段的状态。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值