1、多态介绍
面向对象三大特征:封装、继承、多态。多态是Java面向对象最核心,最难以理解的内容。从一定角度来看,封装和继承几乎都是为多态而准备的。
多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时并不确定,而是在程序运行期间才确定,即一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
比如有一种动物,藏在某个看不见得地方,你知道它是一种动物,但是不知道具体是哪种动物,只有等它发出叫声才能辨别,是猫——喵喵喵,是狗——旺旺旺,你一听就知道这是什么动物,对于不同的动物会有不同的结果,可以理解为多态。
Java多态也可以用一句话表示:父类的引用指向子类的对象。
多态存在的三个必要条件
1、继承:在多态中必须存在有继承关系的子类和父类。
2、重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
3、向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
2、多态的实现
开头说了这么多Java多态的基本概念,那么到底用Java代码怎么体现出来呢?下面我们直接看代码。
public class Animal { public void eat(){ System.out.println("动物吃东西"); } public void skill(){ System.out.println("动物有本领"); } } class Dog extends Animal{ public void eat() { System.out.println("狗吃骨头"); } public void skill(){ System.out.println("狗会看家"); } } class Cat extends Animal{ public void eat() { System.out.println("猫吃鱼"); } public void skill(){ System.out.println("猫会抓老鼠"); } } //测试类 public class AnimalTest { public static void main(String[] args) { Animal dog=new Dog(); dog.eat(); dog.skill(); Animal cat=new Cat(); cat.eat(); cat.skill(); } }
运行结果:
从上面的测试类来看,我们都是创建不同子类的对象,相同的父类引用,却表现出它们不同的特征,这就是体现了Java的多态性。
如果你认为这样还是不能体现多态的好处,我们在AnimalTest类中添加一个show()方法,来体会Java多态的好处。
如果Java中没有多态的特性会是什么样的,下面我们来看一下。
public class AnimalTest { public static void main(String[] args) { AnimalTest animalTest=new AnimalTest(); animalTest.show(new Animal()); animalTest.show(new Dog()); animalTest.show(new Cat()); } public void show(Animal animal){ animal.eat(); animal.skill(); } public void show(Dog dog){ dog.eat(); dog.skill(); } public void show(Cat cat){ cat.eat(); cat.skill(); } }
可以发现在show()方法中的形参都传入了对象,而且重载3个相同的show()方法,如果我们有十个百个千个类需要传入方法,那么岂不是要重载上千个方法,可见这样代码的冗余非常的大,非常不利于代码的维护。
而如果有多态的话,只需写一个show()方法即可。
public class AnimalTest { public static void main(String[] args) { AnimalTest animalTest=new AnimalTest(); animalTest.show(new Animal()); animalTest.show(new Dog()); animalTest.show(new Cat()); } public void show(Animal animal){ animal.eat(); animal.skill(); } }
从这里可以看出来多态的优点:
1、减少重复代码,使代码变得简洁(由继承保证)。
2、提高了代码的扩展性(由多态保证)。
但是也有缺点:子类单独定义的方法会丢失。后面的向上转型会介绍到。
多态其实是一种虚拟方法调用。在编译期间,只能调用父类中声明的方法,但是在运行期间,实际执行的是子类重写父类的方法。
总结为一句话:编译看左边,运行看右边(可能看到这句话会有点头晕,但是理解下面向上转型的概念就应该能够理解这句话了)。
3、向上转型
子类引用的对象转换为父类类型称为向上转型。通俗地说就是是将子类对象转为父类对象。此处父类对象也可以是接口。
我们用前面多态的例子举例,只是在子类中添加了它们自己的方法,父类中没有定义,如下。
public class Animal { public void eat(){ System.out.println("动物吃东西"); } public void skill(){ System.out.println("动物有本领"); } } class Dog extends Animal{ public void eat() { System.out.println("狗吃骨头"); } public void skill(){ System.out.println("狗会看家"); } //新添加的方法 public void run(){ System.out.println("狗跑得快"); } } class Cat extends Animal{ public void eat() { System.out.println("猫吃鱼"); } public void skill(){ System.out.println("猫会抓老鼠"); } //新添加的方法 public void life(){ System.out.println("猫有九条命"); } } //测试类 public class AnimalTest { public static void main(String[] args) { Animal dog=new Dog();//向上转型成Animal dog.eat(); dog.skill(); //dog.run();//Cannot resolve method 'run()' Animal cat = new Cat();//向上转型成Animal cat.eat(); cat.skill(); //cat.life();//Cannot resolve method 'life()' } }
这里就产生了向上转型,Animal dog= new Dog();Animal cat= new Cat();将子类对象Dog和Cat转化为父类对象Animal。这个时候Animal这个引用调用的都是子类方法。再去调用子类单独的方法就会报错。如果非要调用也不是说不可以,那就要强转了。既然现在已经是父类了,那就强转为子类呗。
((Dog) dog).run();
((Cat) cat).life();
这样也可以调用。但是千万要注意,不能这样转,子类引用不能指向父类对象,Dog dog=(Dog)newAnimal();Cat cat = (Cat)new Animal();这样是绝对不行的。就好像儿子可以生出爸爸一样,这样不和常理。
如果在向上转型时,子类并没有重写父类的方法,那么调用的就是父类中的方法。
到此为止,也可以证明前面说的一个结论:向上转型会使子类单独定义的方法会丢失。
4、向下转型
与向上转型相对应的就是向下转型了。向下转型是把父类对象转为子类对象。这里我们就会想到,即然子类向上转型为了父类,而子类又继承了父类的属性和方法,为什么还要将父类转型为子类。是因为对象的多态性只适用于方法,而不适用于属性。所以当我们在使用多态的时候,就不能调用子类中的属性和特有的方法了,所以需要向下转型。(内存中实际上是加载了子类所特有的属性和方法,但是由于变量声明的是父类类型,导致在编译时只能调用父类中声明的属性和方法,子类特有的属性和方法不能调用)
我们还是使用向上转型那里的代码为例(Anima、Dog、Cat类):
public class AnimalTest { public static void main(String[] args) { Animal dog=new Dog();//Dog向上转型成Animal Dog dog1= (Dog) dog;//向下转型为Dog dog1.eat(); dog1.skill(); Cat cat=(Cat) dog;//java.lang.ClassCastException: com.thr.java2.Dog cannot be cast to com.thr.java2.Cat cat.eat(); cat.skill(); } }
运行结果:
我们可以发现向下转型为Dog没有报错,但是转型为Cat却报错了,这个倒不难理解,因为开始向上转型本来是Dog,然后再变回Dog,总不能Dog变成Cat吧。所以会报类型转换错误。
向下转型注意事项
- 向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型)
- 向下转型只能转型为本类对象(猫是不能变成狗的)。
向下转型我们一般会使用 instanceof 关键字来判断:
使用方法:a instanceof A:判断对象a是否为对象A的实例,如果是,返回true,如果不是,则返回false。
public class AnimalTest { public static void main(String[] args) { AnimalTest test=new AnimalTest(); test.show(new Animal()); test.show(new Dog()); test.show(new Cat()); } public void show(Animal animal){ if (animal instanceof Dog){ Dog dog= (Dog) animal; dog.eat(); dog.skill(); dog.run(); } } }
运行结果:
我们可以发现测试方法调用三次show方法,分别传入了Animal、Dog、Cat对象,由于show()方法只判断了Dog是否是该对象,所以Dog返回了true,输出了Dog的信息,而其他的返回了false,则没有输出然后信息。
5、经典案例
Java的多态和转型都了解以后,现在趁热打铁,来点网上多态非常经典的例题:
来源:https://blog.csdn.net/Jian_Yun_Rui/article/details/52937791
public class A { public String show(D obj) { return ("A and D"); } public String show(A obj) { return ("A and A"); } } public class B extends A{ public String show(B obj){ return ("B and B"); } public String show(A obj){ return ("B and A"); } } public class C extends B{ } public class D extends B{ } public class Test { public static void main(String[] args) { A a1 = new A(); A a2 = new B(); B b = new B(); C c = new C(); D d = new D(); System.out.println("1--" + a1.show(b)); System.out.println("2--" + a1.show(c)); System.out.println("3--" + a1.show(d)); System.out.println("4--" + a2.show(b)); System.out.println("5--" + a2.show(c)); System.out.println("6--" + a2.show(d)); System.out.println("7--" + b.show(b)); System.out.println("8--" + b.show(c)); System.out.println("9--" + b.show(d)); } }
运行的结果:
前面3个强行发现还能得到答案,但是从第4个之后就有点头晕。
我们来慢慢分析第4个:首先是子类B类向上转型为父类A,而子类B有重写了父类A中的show(A obj)方法,所以a2变量能调用的只有父类A类中的show(D obj)和子类B中的show(A obj),而B是继承自A类的,D继承自B类,所以不可能调用show(D obj)方法,所以结果是4--B and A;剩下的依次类推。
当父类对象变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。但是它仍然要根据继承链中方法调用的优先级来确认方法,该优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
6、总结
通过上面对多态的学习,可以小结一下概念:
1、Java多态也可以用一句话表示:父类的引用指向子类的对象。
2、运行时多态的前提:继承,重写,向上转型。
3、多态能够减少重复代码,使代码变得简洁;提高了代码的扩展性。
4、多态其实是一种虚拟方法调用。归结为一句话:编译看左边,运行看右边。
5、向上转型就是是将子类对象转为父类对象。
6、继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。