前言
面向对象有三大特征:封装、继承、多态。
封装隐藏了类的内部实现机制,可以在不影响使用者的前提下改变类的内部结构,继承是为了重用父类代码,而多态呢?今天我就谈谈自己对多态的理解。
多态
多态是指同一消息可以根据发送对象的不同而采用多种不同的行为方
式。多态具有以下几个优点:
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
多态的形式:1
Parent p = new Child();
向上转型
要理解多态,首先需要了解向上转型。例如我定义了一个Shape类,子类Circle继承自Shape类,实例化一个Circle对象,可以这样表示1
Shape s = new Circle();
简单来说,就是:父类引用指向子类对象。
那么向上转型有啥好处呢?首先我们来看看如果没有向上转型:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public class Shape {
public void draw(){
System.out.println("draw shape");
}
}
public class Circle extends Shape{
public void draw(){
System.out.println("draw circle");
}
}
public class Square extends Shape{
public void draw(){
System.out.println("draw square");
}
}
public class Painter{
public static void main(String[] args){
Painter painter = new Painter();
Circle c = new Circle();
painter.draw(c);
}
public void draw(Circle c){
c.draw();
}
public void draw(Square s){
s.draw();
}
}
最后将打印1
draw circle
这么做是可以的,但是有个主要缺点,若我们需要添加一个新的Shape子类,则必须要在Painter中添加一个新的draw()方法,若遇到需要大量Shape子类工作的情况呢,这个将变为很糟糕,因此,多态就很好地帮我们解决了这个问题。
若使用多态,Painter类只需要这样设计。1
2
3
4
5
6
7
8
9
10
11public class Painter{
public static void main(String[] args){
Painter painter = new Painter();
Circle c = new Circle();
painter.draw(c);
}
public void draw(Shape s){
s.draw();
}
}
当Circle实例传给draw()时,draw()会将Circle实例当做Shape对象,因此对Shape所做的任何操作都将被Circle所接收到。当然,这也是有前提的,Shape的子类必须重写
Shape的方法。若子类没有重写父类的方法,则最终会调用的是父类中的方法,因此最好将抽象的部分设为抽象方法,这样子类在继承的时候若没有重写,编译器将会报错。
绑定
我们只需要子类重写父类方法,在需要的时候将子类实例传给父类引用,便可完成向上转型。那么编译器是如何区分传给父类引用的是哪个子类实例呢,其实编译器是一直不知道对象的类型,但JAVA提供了一种解决办法,后期绑定,也就是在运行时根据对象的类型进行绑定。因此后期绑定也叫动态绑定或运行时绑定。
《JAVA编程思想》中提到,Java中除了static方法和final方法(private方法属于final方法)之外,其他所有的方法都是动态绑定。这意味着通常情况下,我们不必判定是否应该进行后期绑定。若将方法设为final类型,不仅可以防止其他人重写该方法,也可以有效地”关闭”动态绑定。
动态绑定内部机制
为了提高动态分派时方法查找的效率,JVM 会在链接类的过程中,给类分配相应的方法表内存空间。每个类对应一个方法表。
一个类的方法表包含类的所有方法入口地址,从父类继承的方法放在前面,接下来是接口方法和自定义的方法。当我们调用某个方法时,JVM会从方法表中查找相应的方法,其过程如下:
- 首先编译器确定对象的声明类型和方法名。然后找当前类中方法名字匹配的所有方法(由于重载,可能存在多个),然后在其父类中也找类似的属性为public的方法;
- 编译器查看调用方法的参数类型,先在本类中找,然后在超类中找,这一过程称为重载解析(overloading resolution)。若没找到,或在同一个类中找到多个,均报错。
- 若为private、static或者final修饰的方法,为静态绑定,可直接知道调用的是哪个方法,此情况下就省去了剩下的步骤;
- 在程序运行时,JVM会根据对象的实际类型从方法表中调用最合适的方法。
可扩展性
由于引入了多态机制,我们在对现有的代码进行扩展时,而不需要修改现有的方法。还是以Shape为例,向其添加一个size()方法,并在子类中实现该方法,即使如此,我们也不必修改Painter中draw()方法,原代码依然可以稳健运行。具体实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public class Shape {
public void draw(){
System.out.println("draw shape");
}
public void size(){
//TODO
}
}
public class Circle extends Shape{
public void draw(){
System.out.println("draw circle");
}
public void size(){
//TODO
}
}
public class Square extends Shape{
public void draw(){
System.out.println("draw square");
}
public void size(){
//TODO
}
}
public class Painter{
public static void main(String[] args){
Painter painter = new Painter();
Circle c = new Circle();
painter.draw(c);
}
public void draw(Shape s){
s.draw();
}
}
这个例子很好地体现了多态的特性,我们对代码所做的修改,不会对程序中其他不应受到影响的部分产生破坏。
向下转型类型判断
由于向上转型会丢失具体的类型信息,比如Shape的子类Circle中有额外的color()方法,将Circle实例转为Shape类型,这样做是安全的,因为父类不会具有大于子类的接口,因此通过父类调用的方法都是可行的。
而对于向下转型,我们无法知道一个父类会转为哪个子类类型,因此也无法确保被调用的方法是那个类中所含有的。如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public class Shape {
}
public class Circle extends Shape{
public void color(){
System.out.println("paint yellow");
}
}
public class Square extends Shape{
public void size(){
System.out.println("40 x 40");
}
}
public class Painter{
public static void main(String[] args){
Shape shape = new Circle();
Square square = (Square)shape;
square.size(); // ClassCastException
}
}
将Shape实例强转为Square类型,编译器是不会报错的,因为Square是Shape的子类。当用强转后的Square实例调用Circle中的color()方法,编译器就会报一个ClassCastException错误。
为解决上述问题,我们可以使用 ’instanceof关键字‘ 来确保不会出现ClassCastException错误。
将Painter改为:1
2
3
4
5
6
7
8
9public class Painter{
public static void main(String[] args){
Shape shape = new Circle();
if(shape instanceof Square){
Square square = (Square)shape;
square.size();
}
}
}
参考
- 《JAVA编程思想》
- 深入理解JVM方法调用的内部机制