概述 一种事物,多种形态。即同一接口的不同实现方式。简单的理解就是同一个行为有多个不同表现形式或形态的能力。
前提
- 要有继承关系
- 要有方法重写
- 父类引用指向子类对象
- 父类引用调用子类重写方法
优点(接口统一)
- 提高代码可维护性(继承保证)
- 提高代码扩展性(多态保证)
- 实际开发中,往往作为函数参数(父类引用,可以接受任意子类对象)
缺点
- 父类引用指向子类对象,不能通过父类引用调用子类新增方法
Java中多态的体现
public class Test {
public static void main(String[] args) {
Animal a = null;
a = new Dog(); //向上转型
a.eat();
a = new Cat();
a.eat();
a = new Tiger();
a.eat();
// a.run(); //无法调用子类特有方法
}
}
class Animal {
public void eat() {
System.out.println("动物在吃东西");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("Dog 在吃骨头!");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("Cat 在吃鱼!");
}
}
class Tiger extends Animal {
public void eat() {
System.out.println("Tiger 在吃肉");
}
public void run(){
System.out.println("我在奔跑");
}
}
运行结果:
Dog 在吃骨头!
Cat 在吃鱼!
Tiger 在吃肉
在这里,每个子类重写父类的eat()
方法,将子类对象的引用赋给父类对象(即向上转型)。
向上转型过程中需要注意到:
- 向上转型时,子类特有的方法会丢失,例如上述的
Tiger
类中的run()
方法,Animal
对象实例是无法调用的,因为子类引用不能指向父类对象。
向上转型的好处
- 减少重复代码,使得代码更简洁
- 有利于提高系统的扩展性
同样与向上转型相对的就是向下转型,即把父类对象转为子类对象。如下:
Animal a=new Tiger();
Tiger t=(Tiger)a;
t.eat(); //输出 Tiger 在吃肉
Animal a=new Animal();
Tiger t=(Tiger)a;
t.eat(); //程序报错 java.lang.ClassCastException
Cat c=((Cat)t);
c.eat(); // 程序报错 java.lang.ClassCastException
至于为什么会出现上面这种情况?如下解释:
第一段代码的话,a
本身就是Tiger
对象,所以向下转型为Tiger
,肯定无误。第二段代码是要将父类对象强制转换为子类对象,这显然不行,因为子类的内存空间要大于父类空间,强制转换之后,只是覆盖了子类空间的一部分,而另一部分也就相当于是未定内存,但父类对象可以指向,这在编程中是不可能的,所以必然发生错误。第三段代码则更不可行,将一种动物又怎么能转换为另一种动物?
所以,到这里,我们应该已经知道:
向下转型的前提是父类对象指向的是子类对象(也即在向下转型之前,先得向上转型)
所以我们加入instanceof
关键字,来判断是否可以向下转型.
回到上面的代码,我们可对代码做如下更改,使得每次传入一种动物,分别调用其方法:
public void eat(Animal a){
if(a instanceof Dog){
Dog d=(Dog)a;
d.eat();
}
if(a instanceof Cat){
Cat c=(Cat)a;
c.eat();
}
if(a instanceof Tiger){
Tiger t=(Tiger)a;
t.eat();
t.run(); //调用子类特有的run方法
}
}
最后我们分析一个多态案例
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)); //A and A
System.out.println("2=> "+a1.show(c)); //A and A
System.out.println("3=> "+a1.show(d)); //A and D
System.out.println("4=> "+a2.show(b)); //B and A
System.out.println("5=> "+a2.show(c)); //B and A
System.out.println("6=> "+a2.show(d)); //A and D
System.out.println("7=> "+b.show(b)); //B and B
System.out.println("8=> "+b.show(c)); //B and B
System.out.println("9=> "+b.show(d)); //A and D
}
}
class A {
public String show(D d) {
return "A and D";
}
public String show(A a) {
return "A and A";
}
}
class B extends A {
public String show(B b) { //B类新增方法
return "B and B";
}
public String show(A a) { //重写父类show()方法
return "B and A";
}
}
class C extends B {
//拥有show(B b)
//拥有show(A a)
//拥有show(D d)
}
class D extends B {
//拥有show(B b)
//拥有show(A a)
//拥有show(D d)
}
运行结果如下:
1=> A and A
2=> A and A
3=> A and D
4=> B and A
5=> B and A
6=> A and D
7=> B and B
8=> B and B
9=> A and D
相信你看到的时候,同我第一次看到一样傻眼,为什么会这样?
当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,
引用变量类型决定可调用的方法。如果子类中没有覆盖该方法,那么会去父类中寻找。
让我们来看一个知识点:
继承链中对象方法的调用的优先级:
this.show(O),
super.show(O),
this.show((super)O),
super.show((super)O).
上述例子中:C/D
继承B
,B
又继承于A
首先,a2是类型为A的应用类型,其指向类型为B的对象,A确定可以调用的方法为show(D d)和show(A a).
a2.show(b)也就是this.show(b),此处的this指的是B。
然后,在B类中找到show(B b),但是a2无法调用,所以向上一级寻找调用方法:
super.show(O),super指的是A。
在A中找show(B b),没有定义,继续向上找:this.show((super)O),this指的是B
所以在B中找到show((A)O),即show(A a),调用该方法
最后输出:B and A
下面再分析一下第二个:b.show(d);
b为B类型的引用,指向B类型对象,所以只调用本类方法。
在B中找show(D d),存在该方法,直接调用。
输出:A and D
因为分析方法一样,就不在一一作解。掌握了上述这个例子,相信Java中的多态已经掌握的很好了。
总结
- 多态,就是一个行为可有多个不同的表现形式
- 分类:运行时多态和编译时多态
- 运行时多态的前提:继承,重写,向上转型
- 向上/向下转型
- 继承链中对象方法调用的优先级:
this.show(O),super.show(O), this.show((super)O),super.show((super)O)