直接说什么是多态性太抽象了,我们先引入一个例子:
现在我定义了一个feed方法,在不同的类的对象调用这个方法时,都要改变形参,即每当我的对象不同时,都要重载该方法,这显然是很费时间,也不方便的,这时候就引入多态的概念。在这里我们可以认为形参既能接受猫的对象,也能接受狗的对象。因为猫和狗都是动物类的子类。
多态:同一个对象,在不同时刻表现出来的不同形态。
多态的几点细节:
父类引用指向子类对象:
父类 父类对象名 = 子类 子类对象名
多态是继封装、继承之后,面向对象的第三大特性。
多态是出现在继承或者实现关系中的。
多态体现的格式:
父类类型 变量名 = new 子类/实现类构造器;
变量名.方法名();
多态的前提:有继承关系,子类对象是可以赋值给父类类型的变量。例如Animal是一个动物类型,而Cat是一个猫类型。Cat继承了Animal,Cat对象也是Animal类型,自然可以赋值给父类类型的变量。
当使用多态方式调用成员变量时,首先检查父类中是否有该成员变量,如果没有,则编译错误;如果有,则执行的是父类里的成员变量。
总结起来就是:编译看左边,运行看左边。
理解比较困难,直接看代码演示:
创建一个父类,创建一个子类,让子类继承父类,现在实现多态,就是让父类引用指向子类对象。即Fu fu=new Zi(),现在打印父类引用的成员变量,发现打印的是父类的成员变量,这就是执行看左边,也就是说执行的是父类的成员变量。那么什么是编译看左边呢?如果我们把父类的成员变量name注释掉。如下:
发现已经出现错误了,原来编译看左边的意思就是说在编译的时候左边的父类里必须得有name这个成员变量,如果没有就会报错。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,执行的是子类重写后方法。如果子类没有重写该方法,就会调用父类的该方法。(也就是调用子类的方法时,父类必须有该相同的方法)
总结起来就是:编译看左边,运行看右边。
直接看代码:
意思同上,执行看右边就是说,执行的是右边子类的方法。如果把父类的该方法注释掉,会报错如下,这也就是编译看左边的意思。
说白了就是子类的所有方法,在父类中也得全部有,但这显然是不可能的,这也就是多态的弊端:不能使用子类特有的功能!!(后面的转型可以解决该问题)
注意,在多态中,不管是成员变量还是成员方法,父类中一定得有,否则编译报错。至于对成员方法执行看右边,当子类没有重写该成员方法时,就执行相应父类的方法。子类可以没有该方法,但是父类必须得有。
再看一个练习:
定义父类:
public class Animal { public void eat() { System.out.println("动物吃东西!"); } }
定义子类:
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) { // 多态形式,创建对象 Animal a1 = new Cat(); // 调用的是 Cat 的 eat a1.eat(); // 多态形式,创建对象 Animal a2 = new Dog(); // 调用的是 Dog 的 eat a2.eat(); } }
回想文章最开始那个问题,现在就可以用多态来解决了:
多态: 是指同一行为,具有多个不同表现形式。
从上面案例可以看出,Cat和Dog都是动物,都是吃这一行为,但是出现的效果(表现形式)是不一样的。
前提【重点】
有继承或者实现关系
方法的重写【意义体现:不重写,无意义】
父类引用指向子类对象【格式体现】
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
当一个方法既然接收A类的对象,又要接收B类的对象。如果A和B又有共同的父类,方法的形参可以写父类类型。 此时方法中,会根据传递过来的对象,动态的调用不同子类中的方法。
实际开发的过程中:
1,方法的形参是一个非抽象类时,在调用方法时,可以传递父类对象,可以传递所有的子类对象。
2,方法的形参是一个抽象类时,在调用方法时,可以传递所有的子类对象。
3,方法的形参是一个接口时,在调用方法时,可以传递所有的实现类对象。
看一个案例:
定义父类:
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都可以完成。从而实现了实现类的自动切换。
所以,多态的好处,体现在,可以使程序编写的更简单,并有良好的扩展。
注意多态的引入不是说让你以后创建对象都要让父类对象指向子类对象,而是作为形参用来进行传递参数的,搞懂上面那个练习我想就能搞懂多态的作用。
调用成员变量时:编译看左边,运行看左边
调用成员方法时:编译看左边,运行看右边
代码示例:
Fu f = new Zi(); //编译看左边的父类中有没有name这个属性,没有就报错 //在实际运行的时候,把父类name属性的值打印出来 System.out.println(f.name); //编译看左边的父类中有没有show这个方法,没有就报错 //在实际运行的时候,运行的是子类中的show方法,如果子类没有重写该方法则执行父类的该方法 f.show();
我们已经知道多态编译阶段是看左边父类类型的,如果子类有些独有的功能,此时多态的写法就无法访问子类独有功能了。除非父类有子类的全部方法,(这显然是不可能的),否则会编译报错。
看一个案例:
class Animal{ public void eat(){ System.out.println("动物吃东西!") } } 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("吃骨头"); } } class Test{ public static void main(String[] args){ Animal a = new Cat(); a.eat(); a.catchMouse();//编译报错,编译看左边,Animal没有这个方法, //不能使用子类特有的方法。 } }
多态的弊端就是无法访问子类特有功能怎么解决这个问题?就需要转型。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
回顾基本数据类型转换
自动转换: 范围小的赋值给范围大的.自动完成:double d = 5;
强制转换: 范围大的赋值给范围小的,强制转换:int i = (int)3.14
多态的转型分为向上转型(自动转换)与向下转型(强制转换)两种。
向上转型(从子到父):多态本身是子类类型向父类类型向上转换(自动转换)的过程,这个过程是默认的。 当父类引用指向一个子类对象时,便是向上转型。 使用格式:
父类类型 变量名 = new 子类类型(); 如:Animal a = new Cat();
原因是:父类类型相对与子类来说是大范围的类型,Animal是动物类,是父类类型。Cat是猫类,是子类类型。Animal类型的范围当然很大,包含一切动物。所以子类范围小可以直接自动转型给父类类型的变量。
向下转型(从父到子):父类类型向子类类型向下转换的过程,这个过程是强制的。 一个已经向上转型的子类对象,将父类引用转为子类引用,可以使用强制类型转换的格式,便是向下转型。
使用格式:
子类类型 变量名 = (子类类型) 父类变量名;
如:Aniaml a = new Cat();
Cat c =(Cat) a;
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误。也就是说,不能调用子类拥有,而父类没有的方法,即子类特有的方法。编译都错误,更别说运行了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做向下转型。
转型演示,代码如下:
定义类:
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 watchHouse() { System.out.println("看家"); } }
定义测试类:
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 Cat c = (Cat)a; //向下转型后就可以调用子类特有的方法了 c.catchMouse(); // 调用的是 Cat 的 catchMouse } }
转型的过程中,一不小心就会遇到这样的问题,请看如下代码:
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse 【运行报错】 } }
这段代码可以通过编译,但是运行时,却报出了 ClassCastException
,类型转换异常!这是因为,明明创建了Cat类型对象,运行时,当然不能转换成Dog对象的。
为了避免ClassCastException的发生,Java提供了 instanceof
关键字,给引用变量做类型的校验,格式如下:
变量名 instanceof 数据类型 如果变量属于该数据类型或者其子类类型,返回true。 如果变量不属于该数据类型或者其子类类型,返回false。
所以,转换前,我们最好先做一个判断,代码如下:
public class Test { public static void main(String[] args) { // 向上转型 Animal a = new Cat(); a.eat(); // 调用的是 Cat 的 eat // 向下转型 if (a instanceof Cat){ Cat c = (Cat)a; c.catchMouse(); // 调用的是 Cat 的 catchMouse } else if (a instanceof Dog){ Dog d = (Dog)a; d.watchHouse(); // 调用的是 Dog 的 watchHouse } } }
多态的总结:
注意:就是那个父类可以是爷爷类,甚至更高一层的类,当然他们也统称为父类。
注意(超级重点!!):1、当接口作为形参时,所有实现这个接口的类的对象都可以作为实参进行传递!这个是本节的一个重点。
2、.当一个方法的形参是一个类,那么我们可以传递这个类的对象和这个类所有的子类对象
3、当一个方法的形参是一个抽象类,可以传递这个类所有的子类对象
多态的内存(了解,科班出身的最好理解)