面向对象之多态[向上/向下]转型

概述 一种事物,多种形态。即同一接口的不同实现方式。简单的理解就是同一个行为有多个不同表现形式或形态的能力。

前提

  • 要有继承关系
  • 要有方法重写
  • 父类引用指向子类对象
  • 父类引用调用子类重写方法

优点(接口统一)

  • 提高代码可维护性(继承保证)
  • 提高代码扩展性(多态保证)
  • 实际开发中,往往作为函数参数(父类引用,可以接受任意子类对象)

缺点

  • 父类引用指向子类对象,不能通过父类引用调用子类新增方法

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继承BB又继承于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)
上一篇:Problem C: 动物要吃饭


下一篇:Java中的重载和重写及区别