深入理解Java的多态机制

Java多态原理和使用

多态的概念

面向对象有三大特性,继承,封装,多态。

多态表示指在编程中不能确定引用对象指向的具体类型或是方法调用,而要在程序运行时才能确定。

该引用变量的方法调用到底是那个类实现的方法,必须在程序运行期间才能确定,这样不用修改程序源码就可以让引用变量动态的绑定到不同的类实现上从而导致该引用调用的方法随之改变。

不修改程序代码就可以改变程序运行时绑定的具体代码,程序可以选择多个运行状态,这就是面向对象语言的多态性。

多态主要分为编译时的多态和运行时的多态。

一个典型的多态问题

相关类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A{
public String show(D obj){
return ("A and D");
}
public String show(A obj){
return ("A and A");
}
}
class B extends A{
         public String show(B obj){
                return ("B and B");
         }
         public String show(A obj){
                return ("B and A");
         } 
}
class C extends B{} 
class D extends B{}

问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        A a1 = new A();
        A a2 = new B();
        B b = new B();
        C c = new C(); 
        D d = new D(); 
        System.out.println(a1.show(b));   ①
        System.out.println(a1.show(c));   ②
        System.out.println(a1.show(d));   ③
        System.out.println(a2.show(b));   ④
        System.out.println(a2.show(c));   ⑤
        System.out.println(a2.show(d));   ⑥
        System.out.println(b.show(b));     ⑦
        System.out.println(b.show(c));     ⑧
        System.out.println(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

一般来说,方法调用的优先级如下:

this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O).

比如说4,因为a1指向的是一个B的对象,所以B重写了A中show(A obj)方法,所以实际会调用B中show(A obj)方法。

重写和重载

运行时的多态性时面向对象程序设计语言设计代码宠用的一个最强大的特性,动态性的概念可以描述为“一个接口,多个方法”。

Java实现运行时多态性的基础是动态方法调度,是一种在运行时而不是在编译期调用重载方法的机制。

方法的重写(Overriding)和方法的重载(Overloading)是Java多态性的不同表现。

  • Overriding 是父类和子类之间多态性的一种表现,重写是指子类中定义的某个方法与其父类有相同的名称和参数,我们说该方法被重写(overrding)。子类对象使用该方法时,将调用子类中的定义,对于父类的定义如同被屏蔽了。
  • Overloading是指在一个类中定义了多个同名的方法,或者有不同的参数个数,或者有不同的参数类型,则称为方法的重载。Overloaded的方法还可以改变返回值的类型。
多态的缺陷

向上转型是有局限性的:在向上转型中,可以调用的方法就是父类中有的,但是子类中没有的方法,和子类中重写父类的方法

多态代码的第一个缺陷

1
2
3
4
5
6
7
8
9
10
11
public class Instrument {
private void play() { System.out.println("Instrument play"); }
public static void main(String[] args) {
Instrument ins = new Piano();
ins.play();
}
}

class Piano extends Instrument {
public void play() { System.out.println("Piano play"); }
}
1
2
//output
Intrument play

这里子类对象实例化后赋值给父类对象,这里就是向上转型,那么根据向上转型的特点,ins可以调用父类中有,而子类没有的方法和子类重写的方法.

但是这里父类是private 方法,而private方法默认为final方法,final类方法不可以被继承,这旧说明Piano类中的play方法是一个全新的方法,所以ins不能调用子类的play()方法。

第二个缺陷:域和静态方法, 域和静态方法和多态无关。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Super {  
public int field = 0;
public int getField() { return field; }
}

class Sub extends Super {
public int field = 1;
public int getField() { return field; }
public int getSuperField() { return super.field; }
}

public class FieldAccess {
public static void main(String[] args) {
Super sup = new Sub(); // Upcast
System.out.println("sup.field = " + sup.field +
", sup.getField() = " + sup.getField());
Sub sub = new Sub();
System.out.println("sub.field = " +
sub.field + ", sub.getField() = " +
sub.getField() +
", sub.getSuperField() = " +
sub.getSuperField());
}
}
1
2
3
// Output: 
sup.field = 0, sup.getField() = 1
sub.field = 1, sub.getField() = 1, sub.getSuperField() = 0

这段代表表明了当父类和子类拥有名称相同的域时,在向上转型时,子类的域和父类的域是不存在多态的。此外,静态方法也和多态无关。

Java多态的实现原理

多态允许具体访问实现方法的动态绑定,Java对于动态绑定的实现主要依赖于方法表,通过继承和接口的多态实现有所不同。

  • 继承:在执行某个方法的时候,在方法区找到该类的方法表,确认该方法在方法表中的偏移量,找到该方法后如果被重写则直接调用,否则认为没有重写父类该方法,这时候按照继承关系搜索父类的方法表中该偏移量对应的方法。
  • 接口:Java允许一个类实现多个接口,从某种意义上相当于多继承,这样同一个接口的方法在不同类的方法表中的位置可能就不一样了。要通过搜索完整的方法表。

在JVM中:

  • 当程序运行需要某个类的时候,类加载器会将对应的class文件加载到JVM中,并且在方法区建立该类的类型信息(包括方法代码,类变量,成员变量和方法表)。

  • 方法表是实现动态调用的核心,为了优化对象调用方法的速度,方法区的类型信息会增加一个指针,指向记录该类方法的方法表。

Java的方法调用方式

  • Java方法调用有两类
    • 静态方法调用是指对类的静态方法的调用,是静态绑定的。而动态方法调用需要有方法调用所作用的对象,是动态绑定的。
    • 类调用:在编译时就已经确定号具体调用方法的情况。
    • 实例调用:则是在调用的时候才确定具体的调用方法,这就是动态绑定,也是多态要解决的核心问题。

参考文献