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 的实例对象后,可以明确的进行强制类型转换了。至此,也就完成了向下转型。
向下转型比向上转型要麻烦。其实从开头的类比中我们也能理解:
我要一直动物,给我一只猫一条狗或一头猪,都没问题,这就是向上转型;如果我要的是一条蛇,你给的了一个放在黑盒子里的动物(我并不清楚是什么动物),这就无法确定你到底给我什么动物了,这就是向下转型存在的问题。