黑马程序员-多态

-----------android培训java培训、java学习型技术博客、期待与您交流! ------------


一、多态的定义


是指不同类型的对象可以响应相同的消息,从相同的基类派生出来的多个类型可被当作同一种类型对待,可对这些不同的类型进行同样的处理,由于多态性,这些不同派生类对象响应同一方法时的行为是有所差别的。

举例:所有的Object类的对象都响应toString()方法,但具体行为由各自定义。

二、多态的体现


1、父类的引用指向自己的子类对象。  

形式: 父类  对象名=new 子类()

2、父类的引用接收自己的子类对象。  

形式: 在方法上体现,声明方法的时候,形参为父类对象,方法调用时传递的实参是子类的对象。

3、接口的引用指向实现的对象。

形式: 接口 对象名 =new  实现类


三、多态的前提


1、存在着继承或者实现关系
2、有方法的重写(覆盖操作)
3、父类(接口)引用指向子类(实现)对象


四、多态的特点


1、如果子类以父类的身份出现,子类自身特有的行为将会失效
2、如果子类以父类的身份出现,子类如果重写父类的方法,调用的是具体子类的方法(动态绑定)。


五、动态绑定


1、定义

绑定:将一个方法调用同一个方法主体关联起来。

前期绑定:在程序执行前进行绑定(如果有的话,由编译器和连接程序实现)。面向过程默认的绑定方式。

后期绑定:在运行时根据对象的类型进行绑定。也称作动态绑定或运行时绑定。


2、动态绑定原理:

编译器一直不知道对象的类型,对象中安置了某种”类型信息“,在运行时方法调用机制能通过该类型信息找到正确的方法体,并加以调用。


3、例外

java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是自动后期绑定。


4、动态绑定与多态

java中的所有普通方法都是通过动态绑定实现多态的。

这样,我们可以只编写与基类打交道的代码,发送消息给子类对象,让该对象去断定具体应该做什么事。


六、多态中对象调用成员的特点


1、多态不是万能的

只有普通方法的调用可以是多态的。如果你直接访问某个域,这个访问在编译期就将进行解析。代码示例如下:

// Direct field access is determined at compile time.

class Super {
	public int field = 0;

	public int getField() {
		return field;
	}
}

class Sub extends Super {
	public int field = 1;

	public int getField() {
		return field;
	}

	public int getSuperField() {
		return super.field;
	}
}

public class FieldAccess {
	public static void main(String[] args) {
		Super sup = new Sub(); // Upcast
		System.out.println("sup.field = " + sup.field + ", sup.getField() = "
				+ sup.getField());
		Sub sub = new Sub();
		System.out.println("sub.field = " + sub.field + ", sub.getField() = "
				+ sub.getField() + ", sub.getSuperField() = "
				+ sub.getSuperField());
	}
} /*
 * Output: sup.field = 0, sup.getField() = 1 
 * sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0
 */// :~
当Sub对象转型为Super引用时,任何域访问操作都将由编译器解析,因此不是多态的。在本例中,为Super.field和Sub.field分配了不同的存储空间。这样,Sub实际上包含两个称为field的域:它自己的和它从Super处得到的。然而,在引用Sub中的field时所产生的默认域是它自己的而并非Super版本的field域。因此,为了得到Super.field,必须显式地指明super.field。


2、结论

(1)成员方法(因为有重写特性)
编译看左边,运行看右边
解释:在编译时——要查看引用变量所属的类中是否有所调用的方法。如果有,编译通过,如果没有编译失败。
  在运行时——要查看对象所属的类中是否有所调用的方法。
(2)成员变量
编译看左边,运行看左边
解释:只看引用变量所属的类。
(3)静态方法
编译看左边,运行看左边
解释:只看引用变量所属的类。


3、代码示例:

class Fu {
	static int num = 5;

	void method1() {
		System.out.println("fu method_1");
	}

	void method2() {
		System.out.println("fu method_2");
	}

	static void method4() {
		System.out.println("fu method_4");
	}
}

class Zi extends Fu {
	static int num = 8;

	void method1() {
		System.out.println("zi method_1");
	}

	void method3() {
		System.out.println("zi method_3");
	}

	static void method4() {
		System.out.println("zi method_4");
	}
}

class Demo {
	public static void main(String[] args) {
		Fu f = new Zi();
		System.out.println(f.num);
		f.method4();// 多态时候子父类都有的时候打印父类的

		Zi z = new Zi();// 不是多态子类覆盖父类方法用子类的。
		z.method4();
		z.method2();
	}
}


七、多态的好处


1、减少了方法重载之后的冗余。

如果没有多态,要为每一个子类编写接收特定子类引用参数的方法;运用多态,只编写一个接收父类引用参数的方法即可。


2、提高了代码的扩展性。

可以根据自己的需求对系统添加任意多的新子类,那些操纵基类接口的方法不需要任何改动就可以应用于新类。


八、多态的弊端


前期建立父类的引用虽然可以接收后期所有该类的子类对象,但是只能使用父类中的功能,不能使用子类中的特有功能,因为前期的程序无法知道后期的子类的特有内容。也就是说向上转型会丢失具体的子类信息。

解决方法:向下转型


九、向上转型与向下转型


1、定义:

向上转型:对某个对象的引用视为对其基类类型的引用的做法。


2、比较:

(1)向上转型:子类对象引用--->父类类型引用

对于向上转型,程序会自动完成。

//BaseClass为父类,DerivedClass为BaseClass的派生类

BaseClass  bc = new DerivedClass();   //隐式向上转型。

(2)向下转型:父类类型引用--->子类类型引用

对于向下转型,必须明确指明要转型的子类类型

BaseClass  bc = new DerivedClass();   //先向上转型

DerivedClass  dc = (DerivedClass)bc;


3、代码示例:

/*
动物,
猫,狗。

如何使用子类特有方法
 */
abstract class Animal {
	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 kanJia() {
		System.out.println("看家");
	}
}

class Pig extends Animal {
	public void eat() {
		System.out.println("饲料");
	}

	public void gongDi() {
		System.out.println("拱地");
	}
}

class Demo {
	public static void main(String[] args) {

		Animal a = new Cat();// 类型提升。 向上转型。
		function(a);
		Animal a1 = new Dog();
		function(a1);
		// 如果想要调用猫的特有方法时,如何操作?
		// 强制将父类的引用。转成子类类型。向下转型。
		// /Cat c = (Cat)a;
		// c.catchMouse();
		// 千万不要出现这样的操作,就是将父类对象转成子类类型。
		// 我们能转换的是父类应用指向了自己的子类对象时,该应用可以被提升,也可以被强制转换。
		// 多态自始至终都是子类对象在做着变化。
		// Animal a = new Animal();
		// Cat c = (Cat)a;

	}

	public static void function(Animal a)// Animal a = new Cat();用多态
	{
		a.eat();
		if (a instanceof Cat) {
			Cat c = (Cat) a;
			c.catchMouse();
		} else if (a instanceof Dog) {
			Dog c = (Dog) a;
			c.kanJia();
		}
		// instanceof : 用于判断对象的类型。 对象 intanceof 类型(类类型 接口类型)
	}
}

代码示例2:

class A {

	String name = "A";

	public void fun1() {

		System.out.println("A->fun1");

	}

	public void fun2() {

		System.out.println("A->fun2");

	}

}

class B extends A {

	String name = "B";

	public void fun1() {

		System.out.println("B->fun1");

	}

	public void fun3() {

		System.out.println("B->fun3");

	}

}

// 向上转型:
public class Demo {

	public static void main(String args[]) {

		A a = new B();

		a.fun1(); // 输出什么? B->fun1

		a.fun2(); // 输出什么? A->fun2

		// a.fun3(); //error. A中没定义fun3方法

		System.out.println(a.name); // 输出什么? A

	}

}

// 向下转型
public class Demo {

	public static void main(String args[]) {

		// B b = (B)new A( ); //强制转型,运行后抛出异常

		A a = new B();

		B b = (B) a; // 向下转型

		b.fun1(); // 输出什么? B->fun1

		b.fun2(); // 输出什么? A->fun2

		b.fun3(); // 输出什么? B->fun3

	}

}


十、构造器与多态


1、多层构造器的问题:

构造器调用的层次带来了一个两难问题:如果在一个父类构造器的内部调用正在构造的子类对象的某个动态绑定方法,会发生什么情况呢?

在一个普通方法的内部,动态绑定的调用是在运行时才决定的,因为对象无法知道应该调用的方法是属于那个类,还是属于那个类的导出类。

如果父类构造器只是在构建对象过程中的一个步骤,并且该对象所属的类是从这个父类构造器所属的类导出的,那么导出部分在当前父类构造器正在被调用的时刻 仍旧是没有被初始化的。然而,一个动态绑定的方法调用却会向外深入到继承层次内部,它可以调出导出类里的方法,而这个方法所操纵的成员可能还未进行初始化。来看下面这段代码:

// Constructors and polymorphism
// don't produce what you might expect.

class Glyph {
	void draw() {
		System.out.println("Glyph.draw()");
	}

	Glyph() {
		System.out.println("Glyph() before draw()");
		draw();
		System.out.println("Glyph() after draw()");
	}
}

class RoundGlyph extends Glyph {
	private int radius = 1;

	RoundGlyph(int r) {
		radius = r;
		System.out.println("RoundGlyph.RoundGlyph(), radius = " + radius);
	}

	void draw() {
		System.out.println("RoundGlyph.draw(), radius = " + radius);
	}
}

public class PolyConstructors {
	public static void main(String[] args) {
		new RoundGlyph(5);
	}
}
/*
 * Output: 
Glyph() before draw()
RoundGlyph.draw(), radius = 0
Glyph() after draw()
RoundGlyph.RoundGlyph(), radius = 5
*/// :~


Glyph.draw()方法设计为将要被覆盖,这种覆盖是在RoundGlyph中发生的。但是Glyph构造器会调用这个方法,结果导致了对 RoundGlyph.draw()的调用,这看起来似乎是我们的目的。但是如果看到输出结果,我们会发现当Glyph构造器调用draw()方法时,radius不是默认初始值1,而是0。


2、因此,初始化的实际过程是:

(1)在其他任何事物发生之前,将分配给对象的存储空间初始化成二进制的0。

(2)如前所述的那样调用基类构造器。此时,调用被覆盖后的draw()方法(要在调用RoundGlyph构造器之前调用),由于步骤一的缘故,我们此时会发现radius的值为0。

(3)按照声明的顺序调用子类成员的初始化方法。

(4)调用导出类的构造器主体。


3、结论:

编写构造器时有一条有效的准则:用尽可能简单的方法使对象进入优良的状态;如果可以的话,避免调用其他方法。


十一、再论继承与组合的选用


1、选哪个好

更好的方式是首先选择组合,尤其是不能十分确定应该用哪一种方式时。组合不会强制我们的程序设计进入继承的层次结构中。而且,组合更加灵活,因为它可以动态选择不同类型(因此也就选择了不同的行为);相反地,继承在编译时就需要知道确切类型。请看如下代码:

// Dynamically changing the behavior of an object
// via composition (the "State" design pattern).

class Actor {
	public void act() {
	}
}

class HappyActor extends Actor {
	public void act() {
		System.out.println("HappyActor");
	}
}

class SadActor extends Actor {
	public void act() {
		System.out.println("SadActor");
	}
}

class Stage {
	private Actor actor = new HappyActor();

	public void change() {
		actor = new SadActor();
	}

	public void performPlay() {
		actor.act();
	}
}

public class Transmogrify {
	public static void main(String[] args) {
		Stage stage = new Stage();
		stage.performPlay();
		stage.change();
		stage.performPlay();
	}
} /*
 * Output: 
   HappyActor
   SadActor
 */// :~

Stage对象包含一个对Actor的引用,而 Actor被初始化为HappyActor对象。这意味着 performPlay()会产生对应于HappyActor的特殊行为。既然引用在运行时可以与另一个不同的对象重新绑定起来,所以SadActor对象的引用可以在actor中替代HappyActor,于是由performPlay()产生的行为也随之改变。这样一来,我们在运行期间获得了动态的灵活性(也叫状态模式)。


2、结论:

选用继承还是组合,一条通用的准则是:用继承表达行为间的差异,用域(字段)表达状态上的变化。

上例中,两者都用到了:通过继承得到了两个不同的类,用于表达act()方法的差异;而Stage通过运用组合是自己的状态发生变化。在这种情况下,这种状态的改变也就产生了行为的改变。


十二、例题:


编写一个Java应用程序,设计一个汽车类Vehicle,包含的成员属性有:车轮个数wheels和车重weight。小车类CarVehicle的子类,其中包含属性载人数passenger_load。卡车TruckVehicle的子类,其中包含载人数passenger_load和载重量payload。要求每个类都有相关数据的输出方法。编写测试类使用多态输出汽车的信息

/***
 * 
 * 父类
 */

class Vehicle {

	/* 声明方法 */

	private String name;

	double wheels;

	double weight;

	/* getter,setter方法 */

	public String getName() {

		return name;

	}

	public void setName(String name) {

		this.name = name;

	}

	public double getWheels() {

		return wheels;

	}

	public void setWheels(double wheels) {

		this.wheels = wheels;

	}

	public double getWeight() {

		return weight;

	}

	public void setWeight(double weight) {

		this.weight = weight;

	}

	/* 构造方法 */

	public Vehicle(String name, double wheels, double weight) {

		super();

		this.name = name;

		this.wheels = wheels;

		this.weight = weight;

	}

	/* 实例方法 */

	public void go() {

		System.out.println(name + "有" + wheels + "轮子," + "他的重量是:" + weight
				+ "吨");

	}

}

/**
 * 
 ** 小汽车类,子类
 */

class Car extends Vehicle {

	public Car(String name, double wheels, double weight) {

		super(name, wheels, weight);

	}

	@Override
	public void go() {

		super.go();

		int passenger_load = 5;

		System.out.println("他能载" + passenger_load + "个人");

	}

}

/**
 * 
 * 卡车类
 */

class Truck extends Vehicle {

	public Truck(String name, double wheels, double weight) {

		super(name, wheels, weight);

	}

	@Override
	public void go() {

		super.go();

		double passenger_load = 6;

		double payload = 10;

		System.out.println("他的载人量是:" + passenger_load + "\n" + "他的载重量是"
				+ payload + "吨");

	}

}

/*
 * 
 * 测试类
 */

public class Test {

	public void carry(Vehicle a)

	{

		a.go();

	}

	public static void main(String[] args) {

		Test get = new Test();

		get.carry(new Car("小汽车", 4, 5));

		System.out.println("--------------------------");

		get.carry(new Truck("大卡车", 12, 6));

	}

}

/*output:
 小汽车有4.0轮子,他的重量是:5.0吨
他能载5个人
--------------------------
大卡车有12.0轮子,他的重量是:6.0吨
他的载人量是:6.0
他的载重量是10.0吨
*/


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值