在面向对象程序设计语言中,多态是三种基本特性之一(其他两种分别是 抽象、继承)。
多态又称动态绑定、后期绑定、运行时绑定。
我们先来看一个多态的例子:
public class Test {
public static void main(String[] args) {
play(new Cat());
play(new Dog());
}
public static void play(Animal animal) {
animal.enjoy();
}
}
class Animal {
public Animal() {}
public void enjoy() {
System.out.println("叫声...");
}
}
class Cat extends Animal{
public Cat() {}
@Override
public void enjoy() {
System.out.println("喵...");
}
}
class Dog extends Animal {
public Dog() {}
@Override
public void enjoy() {
System.out.println("汪...");
}
}
我们可以看到,在main方法里面我们调用的是play()方法,这个方法的参数是一个Animal类型的对象,但是实际上我们传入的是Animal的子类的对象,而程序运行实际上调用的就是子类的enjoy()方法。像这种 父类引用指向子类对象的 现象就是多态。
多态机制可以有效的消除数据类型之间的耦合度,增加代码的可扩展性与于可读性,改善代码的组织结构,为什么这么说呢,因为在你使用基类接口实现多态机制后,我们只需要编写与基类相关联的代码就可以了,无需关心具体实现的子类以及以后有可能增加的子类,并且这些代码对于所有子类都是可以正确运行的,因为最终程序调用的是你传入的子类所重写的方法。
接下来我们开看看Java在内部是如何实现这种机制的吧,首先你需要知道什么是“绑定”,所谓绑定,就是 将一个‘方法调用’和一个‘方法主体’关联起来,绑定分为两种:
在程序执行前进行绑定,通常是由编译器和连接程序实现,叫做前期绑定,对于上面的程序,当编译器只有两个Animal类型的引用的时候,它无法知道调用的是哪个play()方法。此时,就需要后期绑定,所谓后期绑定,也称动态绑定,它的含义就是在运行过程中根据对象的类型进行方法绑定,后期绑定的机制可以在运行的时候判断对象的类型,从而调用恰当的方法,也就是说,编译器一直不知道对象的实际类型,他所掌握的只是一个基类的引用。
多态通过分离“做什么”与“怎么做”,来将接口与实现分离开,有了多态,就可以很好的提高程序的扩展性、降低类型之间的耦合度,在主业务逻辑代码中(main),我们根本不用考虑传入play()方法中的是什么,只要它是Animal类型,那么就可以正确运行,在以后业务中如果增加一些新的Animal子类型,也无需修改现有的基类方法!
多态的三个重要特征:
1. 继承(或接口实现)
2. 重写(动态绑定子类方法)
3. 父类引用指向子类对象(向上转型)
多态的缺陷:
1. 对于“假覆盖”:
基类中的private方法是不对子类可见的,只有非private方法才能被覆盖,子类中的“覆盖”private方法,实际上是一个全新的、与基类没有关系的方法,此时如果使用多态进行调用,编译器不会报错,但是也不是我们所期望的结果,因为你需要知道,对于子类扩展方法,它们不会暴露在基类的引用视野中。
2. 访问“域”和静态方法
我们先来看这个例子:
public class Test {
public static void main(String[] args) {
Super s = new Sub();
System.out.println(s.getN());
System.out.println(s.n);
}
}
class Super {
public int n = 0;
public int getN() {
return n;
}
}
class Sub extends Super {
public int n = 1;
public int getN() {
return n;
}
public int getSuperN() {
return super.n;
}
}
我们看上面的程序,按照多态机制,我们很容易想象对于子类对象s它所调用的方法在执行期会自动转成自己的方法,而不是父类引用的方法,但是调用域(成员变量)的时候,则不存在多态,因此s.n所调用的依然是父类中的变量,程序执行结果是:1 0,即:只有普通的方法调用可以使多态的!
另外,静态方法是不存在多态的,因为多态本质是基于解决降低型耦合度的一种机制,而静态的方法不是针对于某一个对象,它属于整个类的,因此,静态方法不存在多态。
3. 构造器内部的多态
首先,这其实并不属于一种缺陷,这是一个很有意思的程序,来自于thinking in java :
public class Test{
public static void main(String[] args) {
new Teacher(10);
}
}
class People {
public People() {
System.out.println("before people draw...");
draw();
System.out.println("after people draw...");
}
void draw() {
System.out.println("people draw...");
}
}
class Teacher extends People {
private int r = 1;
public Teacher(int n) {
super();
r = n;
System.out.println("Teacher ... r = "+r);
}
public void draw() {
System.out.println("Teacher draw ... r = "+r);
}
}
我们可以看到Teacher类重写了People类的draw方法,而在父类构造器中调用了draw方法,由于实例化子类必须首先调用父类构造器,因此,隐式的存在了多态现象,我们期望的结果就是:
1>. 调用基类构造器:
1.1>. 输出before people draw...
1.2>. 多态:动态绑定到子类重写的draw方法:Teacher draw ... r = 1
1.3>. 输出after people draw...
2>. 初始化自己的成员
2.1>.赋值
2.2>. 输出Teacher ... r = 100
但是,实际当我们运行程序的时候,其结果却是:
before people draw...
Teacher draw ... r = 0
after people draw...
Teacher ... r = 10
区别在于构建父类对象的时候子类对象的成员变量r的值是0,这就涉及到了初始化的顺序:
首先:在其他任何事物初始化之前,对象的存储空间初始化成二进制0
随后:逐步向下调用基类构造器,上述例子则是调用People构造器,由于上一步的原因,多态所调用的draw方法中的r初始化成了0
然后:按照成员声明顺序进行初始化
最后:执行子类的构造器主体
对于构造器安全的做法:在构造器内,为以安全调用的那些方法是基类中的final方法(当然包括peivate方法,private方法是隐式的final),因为这些方法不能被覆盖!
多态,是一项让程序员“将改变的事物与未变的事物分离开”的重要技术!