多态(面向对象特征之一)
子类对象继承的父类类型或者实现的父接口类型,父类类型的变量指向子类创建的对象,使用该变量调用父类中一个被子类重写的方法,则父类中的方法呈现出不同的行为特征,这就是多态。
多态的引入
多态是继封装、继承之后,面向对象的第三大特性。
生活中,比如跑的动作,小猫、小狗和大象,跑起来是不一样的。再比如飞的动作,昆虫、鸟类和飞机,飞起来也是不一样的。可见,同一行为,通过不同的事物,可以体现出来的不同的形态。多态,描述的就是这样的状态。
多态定义
多态: 多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作
多态的前提:
1. 继承或者实现【二选一】
2. 方法的重写【意义体现:不重写,无意义】
3. 父类引用指向子类对象【格式体现】
多态分为编译时多态和运行时多态:
编译时多态主要指方法的重载
运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
运行时多态有三个条件:
继承,覆盖(重写),向上转型
多态体现:
父类引用或者接口的引用指向了自己的子类对象。 //Animal a = new Cat(); 父类类型 变量名 = new 子类对象; 变量名.方法名(); | Fu f = new Zi(); f.method(); |
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。
多态的好处:
A:提高了代码的维护性(继承保证)
B:提高了代码的扩展性(由多态保证)
实际开发的过程中,父类类型作为方法形式参数,传递子类对象给方法,进行方法的调用,更能体现出多态的扩展性与便利。
定义父类: public abstract class Animal { public abstract void eat(); } 定义子类: class Cat extends Animal { public void eat() { System.out.println("吃鱼"); } } class Dog extends Animal { public void eat() { System.out.println("吃骨头"); } } | 定义测试类: public class Test { public static void main(String[] args) { // 多态形式,创建对象 Cat c = new Cat(); Dog d = new Dog(); // 调用showCatEat showCatEat(c); // 调用showDogEat showDogEat(d); //以上两个方法, 均可以被showAnimalEat (Animal a)方法所替代而执行效果一致 showAnimalEat(c); showAnimalEat(d); } public static void showCatEat (Cat c){ c.eat(); } public static void showDogEat (Dog d){ d.eat(); } public static void showAnimalEat (Animal a){ a.eat(); } } |
由于多态特性的支持,showAnimalEat方法的Animal类型,是Cat和Dog的父类类型,父类类型接收子类对象,当然可以把Cat对象和Dog对象,传递给方法。
当eat方法执行时,多态规定,执行的是子类重写的方法,那么效果自然与showCatEat、showDogEat方法一致,所以showAnimalEat完全可以替代以上两方法。
不仅仅是替代,在扩展性方面,无论之后再多的子类出现,我们都不需要编写showXxxEat方法了,直接使用showAnimalEat都可以完成。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
多态的弊端:
当父类引用指向子类对象时,虽然提高了扩展性,但是只能访问父类中具备的方法,不可以访问子类中特有的方法。(前期不能使用后期产生的功能,即访问的局限性)
多态的机制,重载overlond,重写overread实现多态
重写Overread父类与子类之间多态的表现。继承类,子类继承了父类所有的方法,可以覆盖父类的方法,子类访问权限大于父类.
重载Overlond一个类中多个相同的方法名,他们的参数个数,类型顺序不相同。
多态的出现思想上也做着变化:
以前是创建对象并指挥对象做事情。有了多态以后,我们可以找到对象的共性类型,直接操作共性类型做事情即可,这样可以指挥一批对象做事情,即通过操作父类或接口实现。
Java引用变量有两种类型,分别是编译时类型和运行时类型:编译时类型由声明该变量时使用的类型决定;运行时类型是由该变量指向的对象类型决定。如果编译时类型和运行时类型不一致,就可能出现所谓多态。因此就会将子类对象之间赋值给父类引用变量,称为向上转型,而不用进行类型转换。如 Animal a = new Bird(),其中引用变量 a 的编译时类型是 Animal,运行时类型是Bird
如果编译时类型和运行时类型不一致时未必会出现所谓多态,父类类型的变量指向子类创建的对象,但由于子类中没有重写该变量所调用的父类方法,所以没有出现多态。
多态的上转型对象:
子类实例化的对象赋值给父类声明变量,则该对象称为上转型对象,这个过程称为对象上转型,对应于数据类型转换中的自动类型转换:
上转对象不能操作子类新增的成员变量;不能调用子类新增的方法
上转对象调用父类方法,如果该方法已被子类重写,则表现子类重写后的行为特征,否则表现父类的行为特征。
使用上转型对象调用成员变量,无论该成员变量是否已经被子类覆盖,使用的都是父类中的成员变量:
多态的对象下转型
可以将上转型对象再强制转换为创建该对象的子类类型的对象,即将上转型对象还原为子类对象,对应于数据类型转换中的强制类型转换。
还原后的对象又具备了子类所有属性和功能,即可以操作子类中继承或新增的成员变量,可以调用子类中继承或新增的方法。
注意:不可以将父类创建的对象通过强制类型转换赋值给子类声明的变量。
instanceof运算符
用于判断该运算符前面引用类型变量指向的对象是否是后面类,或者其子类、接口实现类创建的对象。如果是则返回true,否则返回false,如果想用子类对象的特有方法,如何判断对象是哪个具体的子类类型呢?其使用格式如下:
引用类型变量 instanceof 类(也可以是接口)
在强制类型转换之前通过instanceof运算符检查对象的真实类型,可以避免类型转换异常,从而提高代码健壮性。
class 毕姥爷{ void 讲课(){ System.out.println("企业管理"); } void 钓鱼(){ System.out.println("钓鱼"); } } | class 毕老师 extends 毕姥爷{ void 讲课(){ System.out.println("JAVA"); } void 看电影(){ System.out.println("看电影"); } } | class { public static void main(String[] args) { 毕姥爷 x = new 毕老师(); //毕老师对象被提升为了毕姥爷类型。 // x.讲课(); // x.看电影(); //错误. 毕老师 y = (毕老师)x; //将毕姥爷类型强制转换成毕老师类型。 y.看电影();//在多态中,自始自终都是子类对象在做着类型的变化。 } } |
多态在子父类中的成员上的体现的特点:
1,成员变量:在多态中,子父类成员变量同名。
在编译时期:参考的是引用型变量所属的类中是否有调用的成员。(编译时不产生对象,只检查语法错误)
运行时期:也是参考引用型变量所属的类中是否有调用的成员。
简单一句话:无论编译和运行,成员变量参考的都是引用变量所属的类中的成员变量。
再说的更容易记忆一些:成员变量 --- 编译运行都看 = 左边。
2,成员方法。
编译时期:参考引用型变量所属的类中是否有调用的方法。
运行时期:参考的是对象所属的类中是否有调用的方法。
为什么是这样的呢?因为在子父类中,对于一模一样的成员方法,有一个特性:覆盖。
简单一句:成员方法,编译看引用型变量所属的类,运行看对象所属的类。
更简单:成员方法 --- 编译看 = 左边,运行看 = 右边。
3,静态方法。
编译时期:参考的是引用型变量所属的类中是否有调用的成员。
运行时期:也是参考引用型变量所属的类中是否有调用的成员。
为什么是这样的呢?因为静态方法,其实不所属于对象,而是所属于该方法所在的类。
调用静态的方法引用是哪个类的引用调用的就是哪个类中的静态方法。
简单说:静态方法 --- 编译运行都看 = 左边。