一、什么叫多态?
- 多态定义:同一个行为具有多个不同的表现形式或形态的能力
- 多态类型:多态分为编译时多态性和运行时多态性
二、什么是编译时多态,怎么表现的?
此文中介绍过,重载是指在同一个类(也可以是子类和父类)中,同一个方法名被定义多次,但所采用的参数列表不同,在方法重载的编译阶段,编译器会根据参数的不同具体来确定要调用哪个重载的方法。
举个例子说明一下编译时多态性:
public class Example5_3 {
int max(int x){...}
int max(int x,int y){...}
float max(float x,float y){...}
}
类Example5_3 定义了3个max方法,当比较数的大小时,根据数字类型来调用不同的方法,系统根据已确定的要调用的具体的方法函数来实现编译时的多态性。即使改变对象,对象调用的方法依旧不会改变。
编译时多态是静态的,主要指方法的重载,在运行时谈不上多态,而运行时的多态是怎样的?接着看。
三、什么叫多态?多态性?运行时多态?怎么表现的?
我们先来说一个简单的例子。
继承是多态的基础,特别是引用多态时。一个父类可能有很多子类,如:
class Shape{...}
class Circle extends Shape{...}
class Triangle extends Shape{...}
class Rectangle extends Shape{...}
上述的例子中Shape类时父类,Circle 类和Rectangle类但是Shape的子类,一个父类的引用变量可以指向一个子类的实例对象:声明Shape父类的引用变量sp:
Shape sp;
sp指向Shape父类的不同子类对象:
Shape sp = new Circle();
Shape sp = new Triangle ();
Shape sp = new Rectangle ();
这种赋值是将一个子类对象直接赋值给一个父类的引用变量,即将类继承层次结构中的下层对象(子类)转化为上层对象(父类),称为向上类型转换(向上转型),这是由系统自动完成的。
当把一个子类对象直接赋值给父类的引用变量时,如上述的Shape sp = new Circle();语句中的sp引用变量的编译类型为Shape,而运行时的类型为Circle。当运行时调用该引用变量的方法时,其方法行为表现为子类的方法行为特征,而不是父类的方法行为特征,即相同类型的引用变量调用同一个方法时会呈现出多种不同的行为特征,而不是父类的行为特征,这就是多态。
具体来说就是程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时不确定,而在程序运行时才确定。因为在程序运行时才确定具体的类,所以引用变量会绑定到不同的类的实现上,从而导致该引用变量调用的具体方法随之改变,让程序可以选择多个运行状态,这就是多态性。
对于多态性的一个引用,调用方法的简单原则是:Java运行时,系统会根据调用该方法的实例来决定调用哪个方法。对子类的一个实例,如果子类重写了父类的方法,则运行时系统会调用子类的方法;如果子类继承了父类的方法(未重写),则运行时系统会调用父类的方法。即一个父类的引用会根据当前所指向对象的不同调用相应子类中被重写的方法,这就是所谓由引用多态引发的运行多态性。
举个简单的例子说明下:
//先定义一个形状父类
class Shape {
public double area() {
return 0;
}
}
//定义基于形状类的子类圆类
class Circle extends Shape {
private double r;
public Circle(double r) {
this.r = r;
}
@Override
public String toString() {
return "Circle{" +
"r=" + r +
'}';
}
@Override
public double area() {
return 3.14 * r * r;
}
}
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.height = height;
this.width = width;
}
@Override
public String toString() {
return "Rectangle{" +
"width=" + width +
", height=" + height +
'}';
}
@Override
public double area() {
return width * height;
}
}
//测试类
public class Example5_3 {
public static void main(String[] args){
Shape[] shapes = new Shape[3];
shapes[0] = new Circle(3);
shapes[1] = new Rectangle(2,3);
shapes[2] = new Circle(2);
for (int i = 0;i<shapes.length;i++){
System.out.printf(shapes[i].toString()+",面积是:%f",shapes[i].area());//多态性体现
System.out.println();//空行
}
}
}
运行的结果为:
Circle{r=3.0},面积是:28.260000
Rectangle{width=2.0, height=3.0},面积是:6.000000
Circle{r=2.0},面积是:12.560000
由于toString()方法和area()方法都被子类重写,且子类的父类引用都向上转型了,所以调用该些方法时,必定是使用对于子类中定义的这些方法。
四、怎么实现多态?
上面的例子已经简单的实现了多态,下面来简单总结一下。
实现多态有3个必要条件:继承、重写、向上转型
- 继承:多态中必须有存在继承关系的子类和父类
- 重写:子类对父类的某些方法进行重新定义,调用这些方法时就会调用子类的方法
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样引用才能具备技能调用父类方法和子类方法
多态调用一般原则
:对于引用子类对象的父类引用对象,如果子类重写了父类的方法,则运行时父类引用对象会调用子类的方法;如果子类继承了父类的方法(未重写),则运行时父类引用对象会调用父类的方法。
上述的原则可以解释许多普通的多态引用,接着我们来思考一个经典的多态例子:
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));
}
}
输出的结果是
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
如果使用一般原则,从4中看出,a2.show(b),a2是引用了子类B对象的父类A的引用对象,而show(b)方法只是被重载了并没有被重写,于是父类引用对象(a2)会调用父类的方法,发现父类中也没有该方法,那怎么办呢?此刻的一般原则不适用了。其实在继承链中对象方法的调用存在一个优先级
:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)
我们接着来分析一下程序:
- 首先从上面的程序可以看到A、B、C、D的关系
- 接着我们来分析各语句:
序号 | 调用方法 | 调用过程 | 调用结果 |
---|---|---|---|
1 | a1.show(b) | this(A).show(O(B)))☞super(不存在).show(O(B )))☞this(A).show((super)O(A)) ))☞A.show(A) | A and A |
2 | a1.show(c ) | this(A).show(O(C )))☞super(不存在).show(O(C )))☞this(A).show((super)O(A)))☞A.show(A) | A and A |
3 | a1.show(d) | this(A).show(O(D)) )☞A.show(D) | A and D |
4 | a2.show(b) | this(A).show(O(B)) )☞super(不存在).show(O(B )))☞this(A).show((super)O(A)))☞A.show(A))☞B.show(A) | B and A |
5 | a2.show(c ) | this(A).show(O(C )))☞super(不存在).show(O(C )))☞this(A).show((super)O(B或A )) )☞A.show(A))☞B.show(A ) | B and A |
6 | a2.show(d) | this(A).show(O(D)))☞ A.show(D) | A and D |
7 | b.show(b) | this(B).show(O(B)) )☞ B.show(B) | B and B |
8 | b.show(c ) | this(B).show(O(C )))☞super(A).show(O(C )))☞this(B).show((super)O(B)) )☞ B.show(B) | B and B |
9 | b.show(d) | this(B).show(O(D)))☞super(A).show(O(D)) )☞ A.show(D) | A and D |
针对上述调用过程的详细解释:比如4,a2.show(b)
,a2是一个引用变量,类型为A,则this为a2,b是B的一个实例,于是它到类A里面找show(B obj)
方法,没有找到,于是到A的super(超类)找,而A没有超类
,因此转到第三优先级this.show((super)O)
,this仍然是a2,这里O为B,(super)O即(super)B即A
,因此它到类A里面找show(A obj)的方法,类A有这个方法,但是由于a2引用的是类B的一个对象,B覆盖了A的show(A obj)
方法,因此最终锁定到类B的show(A obj)
,输出为"B and A”。
a2是一个引用变量,类型为A,它引用的是B的一个对象,因此这句话的意思是由B来决定调用的是哪个方法。因此应该调用B的show(B obj)
从而输出"B and B”
才对。但是为什么跟前面的分析得到的结果不相符呢?!问题在于我们不要忽略了蓝色字体的后半部分,那里特别指明:这个被调用的方法必须是在超类中定义过的,也就是被子类覆盖的方法。B里面的show(B obj)
在超类A中有定义吗?没有!那就更谈不上被覆盖了。实际上这句话隐藏了一条信息:它仍然是按照方法调用的优先级来确定的。它在类A中找到了show(A obj)
,如果子类B没有覆盖show(A obj)
方法,那么它就调用A的show(A obj)
(由于B继承A,虽然没有覆盖这个方法,但从超类A那里继承了这个方法,从某种意义上说,还是由B确定调用的方法,只是方法是在A中实现而已);现在子类B覆盖了show(A obj)
,因此它最终锁定到B的show(A obj)
。
所以多态机制遵循的原则为:在满足多态的3个必要条件:继承、重写、向上转型后,对于被子类对象赋值的父类引用对象,如果子类重写了父类的方法,则运行时父类引用对象会调用子类的方法;如果子类继承了父类的方法(未重写),则运行时父类引用对象会调用父类的方法。但是父类引用对象依旧要根据继承链中方法调用的优先级来确认具体引用的方法,优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
上述例子的解释如有不当请大家多多指正。
站在巨人的肩膀上,知道的越多也就知道的越少。
参考博文深入理解java多态性