黑马程序员——面向对象15:多态

------- android培训、java培训、期待与您交流! ----------

所谓多态,可以理解为,事物的多种表现形态。举个例子,一名中国人,可以称他为中国人,可以称他为男人,也可以称他为人;再比如,小猫,可以称它叫猫,也可以叫哺乳动物,也可以叫它动物。把上面猫的例子用代码体现为,

Cat cat = new Cat();
mammal = cat = new Cat();
Animal cat = new Cat();
那么,从上面的代码可以看出,Cat实例对象不仅可以表现为Cat类,也可以表现为Animal类。这就是对多态最简单的理解。那么下面我们就从一下六个方面来讲述多态:

(1) 多态的表现形式;

(2) 多态的前提;

(3) 多态的优劣;

(4) 多态中子类方法的调用;

(5) 多态的应用;

(6) 多天中成员的特点。

1. 多态的表现形式

1) 问题的提出

“动物”这个概念是从猫和狗这些具体的动物向上抽象出来的,我们用代码来体现:

代码1:

abstract class Animal
{
	//动物的食物千奇百怪,因此定义一个抽象的“吃”方法
	public abstract void eat();
}
class Cat extends Animal
{
	/*
		猫继承动物类,并复写“吃”方法
		再定义一些猫特有的方法,以显示其特点
	*/
	public void eat()
	{
		System.out.println("吃鱼");
	}
	//猫的特有方法——抓老鼠
	public void catchMouse()
	{
		System.out.println("抓老鼠");
	}
}
class Dog extends Animal
{
	public void eat()
	{
		System.out.println("啃骨头");
	}
	public void smell()
	{
		System.out.println("灵敏的嗅觉");
	}
}
class PolyDemo
{
	public static void main(String[] args)
	{
		Cat cat = new Cat();
		Dog dog = new Dog();
		eat(cat);
		eat(dog);
	}
	//定义“吃”的重载方法,将猫或者狗对象作为参数传入,并调用其“吃”的方法
	public static void eat(Cat cat)
	{
		cat.eat();
	}
	public static void eat(Dog dog)
	{
		dog.eat();
	}
}
运行结果为:

吃鱼

啃骨头

然而,这样的设计有两个问题:1) 动物的种类有千千万万,我们总不能为了动物们“吃”的方法就为每一种动物都定义一个吃的方法(就像上面PolyDemo),这样重复地写代码,效率非常低;2) 如果后期再定义别的Animal子类,就又会面对大面积“改代码”的问题,也就是代码的扩展性非常差

2) 解决问题

我们可以进行这样的思考,猫和狗都是动物,那我们就创建“猫”这种“动物”,创建“狗”这种“动物”,用代码体现就是:

代码2:


Animal cat = new Cat();
Animal dog = new Dog();
cat.eat();
dog.eat();

运行结果为:

吃鱼

啃骨头

这一结果的出现同样也是由继承体系中子类复写父类的方法的机制决定的,而这种创建对象的方式,正是表明了事物具有不同的表现形式——Cat对象既是猫类型又是动物类型,同样,Dog对象既是狗类型也是动物类型。总结一句话就是:父类引用指向了子类对象,这就是多态。以此为基础,我们来修改代码1中PolyDemo类的eat方法。

当我们调用publicstatic void eat(Cat cat)这个方法时,形式参数和实际参数隐含着这样的一个关系Cat cat = new Cat(),此时我们按照多态的形式改为Animal cat = new Cat(),对应的方法就应该改为public static void eat(Animal an)。此时代码1可以改为如下形式:

代码3:

class PolyDemo2
{
	public static void main(String[] args)
	{
		Cat cat = new Cat();
		Dog dog = new Dog();
		eat(cat);
		eat(dog);
	}
	/*
		重新定义“吃”的方法,传入的对象只要是Animal的子类对象,即可接收
		并调用该对象的eat方法
	*/
	public static void eat(Animal an)
	{
		an.eat();
	}
}
运行结果同样是:

吃鱼

啃骨头

这样设计代码,可以大幅度的简化代码,而且具有很高的扩展性,无论定义什么样的子类,只要是Animal的子类对象都可以接收。

2. 多态的前提

那么,通过上述对多态的描述,我们可以总结,如果想要使用多态的特性,那么需要满足两个前提:

1) 转变的两个类之间必须要有一定的关系,要么是继承,要么是实现,就像前面的示例,Cat继承Animal,同样Dog继承Animal。

2) 在继承与实现的同时,子类还需要复写父类的方法,这样在调用子父类的共性方法时,可以按照子类的方式执行,以体现子类的特性。

3. 多态的优劣

1) 优点

通过上面的描述,我们总结如下:

(a) 简化代码;

(b) 提高扩展性。

2) 弊端

通过父类引用指向子类对象的方式创建对象,是不能去调用子类的特有方法的,比如,

代码4:

public static void eat(Animal an)
{
	an.eat();
	//an.catchMouse();编译失败
}
如果执行第二句代码会编译失败。大家可以这样理解:子类总是后于父类出现的,我们不可能预先知道后出现的子类会有什么样的特有方法,因此我们只能按照现有父类的成员内容去掉用方法,这是符合一般的思维逻辑的。

4. 多态中如何使用子类方法

1) 向上转型

Animal an = new Cat(),如前所述,这种创建对象的方式我们称为父类引用指向子类对象,同时,从另一方面来说,这种形式类似于基本数据类型byte转为int的动作——向上转型。之所以称为向上转型,是因为一个继承体系总是十分形象的描绘为从上到下的一个演变过程(就像我的博客《面向对象10:面向对象的三大特点之二——继承》中的继承示例图)。

2) 向下转型

既然我们要使用子类对象的特有方法,并且该对象的引用是从子类向上转型为父类的,那么我们就把这个父类引用再强制转为子类引用,也就是向下转型——Cat cat = (Cat)an。这样一来,就既能调用子父类共性方法,也可以调用子类的特有方法,

代码5:

class PolyDemo3
{
	public static void main(String[]args)
	{
		Animal an = new Cat();
		Cat cat= (Cat)an;
		cat.catchMouse();
 
		//Dogdog = (Dog)an;
	}
}
运行结果为:

抓老鼠


如果执行注释部分代码,可以编译通过,但是在执行的时候会发生类型转换异常——ClassCastException,这也是显然的,子类对象本来就是Cat类,怎么能转换为Dog类呢?

注意:如下代码是错误的,

Animal an = new Animal();//假设Animal不是抽象类
Cat cat = (Cat)an;
大家可以这样理解:上面的代码和多态之间的区别是,多态中创建的对象本质上确实是猫(子类对象),虽然我们称它为动物(父类引用),而上面的代码中创建的对象本质上就是“动物”,定义父类“动物”时,预先并不知道会有“Cat”这一子类,因此这种强转动作是不能发生的。

3) instanceof关键字

如上所述,在进行类型转换的时候,容易出现类型转换异常,因此最好的办法就是在转换以前进行类型判断,如果想将Cat对象的Animal引用强转为Cat引用前去判断该引用是否指向Cat对象。这里需要引入一个新的关键字“instanceof”,代码体现:

代码6:

class PolyDemo3
{
	public static void main(String[]args)
	{
		//用Animal引用指向Cat对象
		Animal an = new Cat();
		//判断an引用是否是Cat类型
		if(aninstanceof Cat)
		{    
			Cat cat= (Cat)an;
			cat.catchMouse();
		}
		//判断an引用是否是Dog类型
		else if(an instanceof Dog)
		{
			Dog dog= (Dog)an;
			dog.smell();
		}
	}
}
运行结果为:

抓老鼠

通过instanceof关键字的判断,就简单规避了类型转换异常的发生。但是,instanceof关键还是有它的弊端的。还是那个问题,如果后期又出现了一批Animal类的子类,就需要改代码。因此,通常在两种情况下使用instanceof关键字:

1) 子类数量较少;

2) 用于调用某个特定子类的特有方法,后面会有具体使用示例。

我们做一个简单的总结:当我们以父类引用指向子类对象的方式创建对象时,该引用既能被提升,也能被强制向下转换,而被转换的对象自始至终是“子类”对象。

5. 多态的应用

还是以汽车为例,为了体现多态的特点,除了定义Vehicle(车辆)类的继承体系以外,我们再定义一个ControlVehicle(车辆控制类)工具类,通过该工具类可以实现对车辆的驾驶与停车动作,而不必单独去操作每一个对象。我们来看代码,

代码7:

/*
	定义抽象Vehicle(车辆)父类
	定义抽象方法drive(驾驶)
	定义park(停车)方法
*/
abstract class Vehicle
{
	public abstract void drive();
	public void park()
	{
		System.out.println("停车");
	}
}
/*
	Sedan(轿车)类继承Vehicle(车辆)类
	复写drive方法,运送乘客
*/
class Sedan extends Vehicle
{
	public void drive()
	{
		System.out.println("运送乘客");
	}
}
/*
	Truck(卡车)类继承Vehicle(车辆)类
	复写drive方法,运输货物
*/
class Truck extends Vehicle
{
	public void drive()
	{
		System.out.println("运输货物");
	}
}
/*
	定义一个专门用于控制所有汽车的类
	通过这个类,可以实现对汽车的驾驶以及停车动作
*/
class ControlVehicle
{
	/*
		通过多态的形式将Vehicle类的子类对象作为参数传入drive方法
		并经过类型判断,调用子类对象drive特有方法
	*/
	public void drive(Vehicle v)
	{
		if(vinstanceof Sedan)
		{
			Sedan s = (Sedan)v;
			s.drive();
		}
		else if(v instanceof Truck)
		{
			Truck t = (Truck)v;
			t.drive();
		}
	}
	/*
		同样,通过多态的形式调用子类对象的park共性方法
	*/
	public void park(Vehicle v)
	{
		v.park();
	}
} 
class PolyDemo4
{
	public static void main(String[] args)
	{
		//通过的多态的形式分别创建两个Vehivle子类对象
		Vehicle s = new Sedan();
		Vehicle t = new Truck();
		//创建Vehicle控制对象
		ControlVehicle cv = new ControlVehicle();
		cv.drive(s);
		cv.park(s);
		cv.drive(t);
		cv.park(t);
             
	}
}
运行结果是:

运送乘客

停车

运输货物

停车

上面例子就是多态的一个简单应用,车辆控制类的出现大大提高了代码的扩展性,即使后期再出现Vehicle类的子类,也可以通过该工具类实现对车辆对象的控制。另外,我们不必再单独控制每个对象,而通过工具类就可以实现批量操作。当然,这个例子只是用于演示,还是应该减少instanceof关键字使用。

6. 多态中成员的特点

我们通过对继承的学习已经知道,在一个继承体系中,子类通过继承“拥有”父类的所有非私有成员,通过创建子类对象,既可以调用子类方法也可调用父类方法;另外,如果子类复写了父类方法,那么在调用该同名方法时就会以子类的特有形式实现。这是继承体系中成员的特点,而在多态机制中成员又有了一些新的变化。

1) 非静态成员方法

代码8:


class Super
{
	public void f1()
	{
		System.out.println("Superf1");
	}
	public void f2()
	{
		System.out.println("Superf2");
	}
}
class Sub extends Super
{
	public void f1()
	{
		System.out.println("Subf1");
	}
	public void f3()
	{
		System.out.println("Subf3");
	}
}
 
class PolyDemo5
{
	public static void main(String[] args)
	{
		//通过多态的形式创建子类对象
		Super s = new Sub();
		s.f1();
		s.f2();
		//s.f3();
	}
}
运行结果为:

Sub f1

Super f2

主函数中,注释部分代码是不能通过编译的,这是因为,在编译期间,还并没有创建子类对象,它只是从语法的角度发现父类中并没有定义f3方法,所以编译失败。

我们再来看结果。即使通过父类引用指向子类对象的方式,f1和f2的调用者最终还是是子类“对象”,既然是子类对象,调用方法时应该去参考子类中的方法。那么这时候由于子类复写了父类的同名方法,因此f1还是以子类的特有方式执行。而f2的执行结果就是简单的继承。

对于多态机制中成员方法的特点总结如下:

(a) 在编译时期,参阅引用型变量所属的类(父类)中是否定义了被调用的方法。如果有,编译通过,反之,编译失败;

(b) 在运行时期,参阅对象所属的类(子类)中是否定义了被调用的方法。如果有,就执行,否则,运行失败。

简单一句话:多态方式创建对象,编译看父类,运行看子类。因为多态机制中成员方法涉及复写,在实际开发中还是比较常见的。

最后,关于多态机制中调用成员方法,我再啰嗦几句:编译时期看父类就不再多说了,而对于运行时期看子类,我们可以这么理解:子类继承父类,并复写父类的方法,总的来说是对父类功能的扩展和增强,否则子类的出现是没有意义的,既然是是一个优化的过程,那么在实际使用时就应该默认的调用子类更优的方法。另外,以多态的方式调用成员方法就像对变量的调用顺序一样:如果被调用的变量,在局部定义过就是用局部变量的值,否则,去找本类成员变量,再不然就去找父类成员变量。同样,方法在执行时也是这个顺序,子类复写了就调用子类的方法,否则,就去找父类,就像f2一样。

2) 成员变量

代码9:

class Super
{
	int num = 10;
}
class Sub extends Super
{
	int num = 400;
}
class PolyDemo6
{
	public static void main(String[] args)
	{
		//父类引用指向子类对象
		Super s1 = new Sub();
		System.out.println("Supernum = "+s1.num);
		//子类引用指向子类对象
		Sub s2 = new Sub();
		System.out.println("Subnum = "+s2.num);
	}
}
运行结果:

Super num = 10

Sub num = 400


通过多态的方式创建对象,并调用对象的成员变量时,打印的是父类的变量值;如果是子类引用指向子类对象的方法创建对象,打印的是子类的变量值。

对这一现象的浅显解释为:创建子类对象的时候,子类对象中既有子类数据也包含父类数据。当通过父类引用指向子类对象的时候,就会去调用父类数据。

对于多态机制中成员变量的特点总结如下:

无论是编译还是运行时期,都要参阅引用型变量所属的类中是否定义了该变量,如果有,就可以通过编译,并运行;否则失败。

但是,就像子父类中定义同名变量相似,上述这种情况在实际开发中通常是不会发生的。

3) 静态成员

代码10:

class Super
{
	//父类中定义两个静态成员
	static int num = 10;
	public static void f4()
	{
		System.out.println("Superf4");
	}
}
class Sub extends Super
{
	//子父类中定义同名静态成员
	static int num = 400;
	public static void f4()
	{
		System.out.println("Subf4");
	}
}
 
class PolyDemo7
{
	public static void main(String[] args)
	{
		//父类引用指向子类对象
		Super s1 = new Sub();
		System.out.println(s1.num);
		s1.f4();
	}
}
运行结果为:

10

Super f4


从结果上看,当子父类定义了同名的静态成员,并通过多态的方式创建对象调用这些成员,执行的都是父类的成员。

对于多态机制中静态成员的特点总结如下:无论编译还是运行,都要参阅引用型变量所属的类中是否定义了该成员,如果有就编译通过,并执行;否则失败,这有点与成员变量是类似的。

对于这个现象的解释是:静态成员是随着类的加载而加载的,它们是可以通过类名调用的,既然可以通过类名调用,就应该参考这个类名所属的类中定义的静态成员,那么在父类引用指向子类对象的情况中,就应该参考父类中是否定义了该成员,也就是说不参考子类对象。

 

小知识点1:静态绑定与动态绑定

对于这部分知识,更为专业解释应该涉及“静态绑定”和“动态绑定”。我们都知道,一旦类被加载以后,类中的静态成员也就跟着加载进静态方法区中了,同时,这些静态成员就会和所属类相绑定,这称为静态绑定。也就是说无论是通过对象还是类名调用静态成员,最终的调用者都是该成员所属的类,而类以及类存在的方法区空间只能有一个,方法的指向是固定的。与此相对,动态绑定就是指方法的调用者不固定,非静态方法区中的this关键字指向哪个对象,那个对象就是方法的调用者。所以,通过多态的方式创建子类对象时,虽然是由父类引用指向的,但是对象是子类类型的,也就是说this指向的是堆内存中的子类对象,所以要参考子类中定义的非静态方法。


你可能感兴趣的:(黑马程序员——面向对象15:多态)