在面向对象(OOP)的程序设计语言中,多态与封装、继承合称为OOP的三大特性。
封装,说简单点就是合并属性和行为创建一种新的数据类型,而继承就是建立数据类型之间的某种关系(is-a),而多态就是这种关系在实际场景的运用。
简单点说,多态就是把做什么和怎么做分开了;其中,做什么是指调用的哪个方法,我是去吃饭(方法a)还是去睡觉(方法b),怎么做是指实现方案,如果我选择吃饭,那么我是吃米饭还是吃面条,”分开了“则是指两件事不在同一时间确定。
说的学术点,多态就是父类的引用指向子类的对象。这样做的好处就是可以消除类型之间的耦合关系。可以看下面的实例:
public class Test1 {
public static void main(String[] args) {
method(new Circle());
}
static void method(Shape shape) {
shape.draw();
}
}
class Shape {
public void draw() {};
public void erase() {}
}
class Circle extends Shape {
public void draw() {
System.out.println("draw circle");
}
public void erase() {
System.out.println("erase circle");
}
}
class Square extends Shape {
public void draw() {
System.out.println("draw square");
}
public void erase() {
System.out.println("erase square");
}
}
class Triangle extends Shape {
public void draw() {
System.out.println("draw triangle");
}
public void erase() {
System.out.println("erase triangle");
}
}
这里通过类Circle,类Square和类Triangle继承类Shape,使得可以在Test1提供统一的访问入口,即方法method();如果没有继承和多态,不仅代码冗余而且类(Circle/Square/Triangle)与类(Test1)之间有耦合。
那么为什么可以通过继承实现这样的功能呢?这就要讨论多态的原理了。
上面的实例中,Shape类是父类,其他三个类是子类;method()方法接收的参数是父类Shape的对象,但调用的时候却传递子类Circle的对象,这就是奇怪之处,也是多态的体现。
这里我们提出一个疑问,method(Shape shape)接受一个Shape引用,那么在这种情况下,编译器怎么样才能知道这个Shape引用指向的是Circle对象,而不是Square对象或Triangle对象呢?
答案是:编译器也不知道。既然编译器不知道,那么肯定就是在代码运行时确定的,具体表现就是确定方法调用以及调用方法对象之间的关系,那么这个关系的确定就称为绑定。由于绑定动作发生在运行时,所以多态也称运行时绑定或者后期绑定,相比于静态的编译期,多态也称为动态绑定。
关于这个问题,TIJ里面是这么说的,如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器不知道对象的类型,但是方法调用机制能够找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会得知,不管怎么样都必须在对象中安置某种”类型信息“。
所以,动态绑定的过程可以理解为下面几个过程:
注意步骤2中发生了类型转换。我们知道基本数据类型可以发生类型转换,如byte,short,char和int之间;那么引用类型之间也可以,但需要满足一定的条件(有继承关系)。那么这里发生的转换是从circle对象转到shape对象,即从子类转换父类,是向上转换。
Java中除了static方法和final方法(private方法属于final方法)是前期绑定,其他所有方法都是后期绑定。
public class Test1 {
public static void main(String[] args) {
method(new Circle());
}
static void method(Shape shape) {
shape.change();
}
}
class Shape {
public void draw() {};
public void erase() {}
public final void change() {
System.out.println("shape change");
}
}
class Circle extends Shape {
public void draw() {
System.out.println("draw circle");
}
public void erase() {
System.out.println("erase circle");
}
}
由于final方法不能被重写,所以直接调用父类的change()方法。
public class Test1 {
public static void main(String[] args) {
new Circle().change(); // ① shape change
new Square().change(); // ② square change
method(new Circle()); // ③ shape change
method(new Square()); // ④ shape change
}
static void method(Shape shape) {
shape.change();
}
}
class Shape {
public static void change() {
System.out.println("shape change");
}
}
class Circle extends Shape {}
class Square extends Shape {
public static void change() {
System.out.println("square change");
}
}
从上面的运行结果可以得出:
除了特别注意private级别的方法不能被继承以及静态方法可以继承但不能表现多态性外,还要注意类的属性也不能表现多态性,但是可以被继承。
public class Test1 {
public static void main(String[] args) {
Shape shape = new Circle();
System.out.println("shape.radius: " + shape.radius + " , shape.getRadius: " + shape.getRadius());
System.out.println("shape.area: " + shape.area + " , shape.getArea: " + shape.getArea());
Circle circle = new Circle();
System.out.println("circle.radius: " + circle.radius + " , circle.getRadius: " + circle.getRadius() + " , circle.getSuperRadius: " + circle.getSuperRadium());
System.out.println("circle.area: " + circle.area + " , circle.getArea: " + circle.getArea());
}
}
class Shape {
public int radius = 0;
public double area = 0.0;
public int getRadius() {
return radius;
}
public double getArea() {
return area;
}
}
class Circle extends Shape {
public int radius = 1;
public int getRadius() {
return radius;
}
public double getArea() {
return area;
}
public int getSuperRadium() {
return super.radius;
}
}
运行结果:
shape.radius: 0 , shape.getRadius: 1
shape.area: 0.0 , shape.getArea: 0.0
circle.radius: 1 , circle.getRadius: 1 , circle.getSuperRadius: 0
circle.area: 0.0 , circle.getArea: 0.0
根据结果可以知道,属性area被子类继承,属性radius被覆盖,所以属性域可以被子类继承,但不具备多态性。对于属性area,子类和父类共同指向同一块内存空间,而对于属性radius,子类和父类独自拥有存储字符串的内存空间,所以radius属性根据引用类型来决定。
构造函数实际上是static方法,所以构造函数不具备多态性。但我们知道子类的每次实例化,父类都会默认执行不带参数的构造方法;这样做的目的是为了让父类自己初始化自个的属性(一般为private)从而构建完整的父类对象共子类使用。
但如果父类构造函数中调用被子类重写的实例方法,会出现什么结果呢?看下面的实例:
public class Test1 {
public static void main(String[] args) {
new Circle(5);
}
}
class Shape {
public Shape() {
System.out.println("shape() before draw()");
draw();
System.out.println("shape() after draw()");
}
public void draw() {
System.out.println("shape draw()");
}
}
class Circle extends Shape {
private int radius = 1;
public Circle(int radius) {
this.radius = radius;
System.out.println("circle.circle(). radius = " + radius);
}
public void draw() {
System.out.println("Circle.draw(). radius = " + radius);
}
}
运行结果:
shape() before draw()
Circle.draw(). radius = 0
shape() after draw()
circle.circle(). radius = 5
new子类Circle对象时,会调用父类Shape的不带参数的构造方法,但是父类构造方法内调用了一个被子类Circle重写的方法draw(),根据结果可知,会调用子类的draw()方法;但是radius属性值却不是默认的1,而是0,这表明Circle对象还没有来得及构建,其radius属性为JVM给的默认值0,如果是引用类型的话则为null。所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
毕!