深入理解系列之JAVA多态机制(重载/重写)

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态(来自百度百科)。所以,按照这个意思其实多态的“主题”是对象,但是实际在我们运用中我们常把“重载”和“重写”称之为“多态”其实这是不严谨的!重载和重写只是多态的存在带来的两种应用表现形式,也就是说正是因为重载和重写我们才看到了多态的“威力”。所以,当我们谈论多态实现机制的时候其实就是在谈重载和重写的实现机制罢了!

问题一、多态的好处是什么?

多态是面向接口编程的实现基础,而面向接口编程可以降低代码之间的耦合度,这样说来比较抽象我们还是通过一个例子来说明:

package Duotai;

public interface Living_beings {
  public void run();
}


class Human implements Living_beings{
  @Override
  public void run() {
    System.out.println("Human running");
  }
}

class Dog implements Living_beings{
  @Override
  public void run(){
    System.out.println("Dog running");
  }
}
package Duotai;

public class Main {

  public void call(Living_beings living_beings){
    living_beings.run();
  }

  public static void main(String[] args) {
    new Main().call(new Human());
    new Main().call(new Dog());
  }
}

输出:
Human running
Dog running

这就是面向接口编程,也就是说我在应用某个类的时候(call方法就是在应用某个类)并不是直接面向这个类而是面向这个接口——即传递这个接口变量,由于JAVA的多态特性,一个接口可以对应不同的“态”,这样我就可以在后续动态的使用某个“态”(如main方法中使用Human这个“态”)和动态的添加这个“态”(如果有新的“生物”加入如cat,则直接实现Living_being并不需要变更call方法,因为面向的是接口所以只要接口需求不变call方法就不需要改变),而这一点正是“重写”的体现!

对于重载,允许同一个方法名具有不同的方法签名,这样调用该方法的时候回根据方法签名的不同而调用我们需要的函数!我们可以在同一类中“重载”,当然也可以对父类的方法进行“重载”——对父类方法重载和重写的不同在于重写相当于覆盖了父类的方法,而重载除了继承了父类的该方法,又相当于添加了一个函数!重载使得代码更加易于区分同一个方法实现不同功能的特点!但是注意当重载遇到多态时,我们需要仔细分析:

package Duotai;

public class Main {

  public void call(Living_beings living_beings){
    living_beings.run();
  }

  public void run(Living_beings living_beings) {
    System.out.println("Now all is running");
  }

  public void run(Human human){
    System.out.println("Now human is running");
  }

  public void run(Dog dog){
    System.out.println("Now dog is running");
  }

  public static void main(String[] args) {
    Living_beings human = new Human();
    Living_beings dog = new Dog();
    new Main().run(human);
    new Main().run(dog);
  }
}

运行结果:
Now all is running
Now all is running

这里run方法被重载(传入不同的参数类型),但是当实际使用的时候,虽然实际的类型分别为Human和Dog,但是实际上方法调用的时候调用的是living_being!这里就必须探讨用于表现“多态”特性的“重载和重写”在JVM中如何实现的问题了!

问题二、多态在JVM虚拟机中的实现机制是什么?

我们看到了对于重写方法run(),方法调用(living_being.run())会根据对象的实际类型(即new出的类型)来确定最终的方法调用,但是到了方法重载则会根据声明的类型来确定到底选择哪个方法重载!之所以这样,是因为涉及了JVM中的静态分派和动态分派!

在讲解静态分派和动态分派之前,我们首先谈论一下“方法调用”的概念!
“方法调用”就是为了解决在如何选择到正确的目标方法的问题,方法调用分为“解析调用”和“分派调用”!在JVM中每个方法的信息其实都以一种常量的形式存在于class文件的常量池中,在类加载的解析阶段会根据方法信息解析并正确的加载到目标方法并执行,这其中有的信息已经完全写入到了class文件中,仅从class文件信息就可解析出来,这种方法调用被称为“解析”,也就是说解析一定是静态的过程,在编译期间就可以完全确定,在类装载的阶段就可以直接把涉及方法的符号引用转变为可确定的直接引用!但是注意分派却是既包含静态的也包含动态的,静态的概念和解析类似。动态是指编译期在编译时的确确定了一个符号引用,但是真正的直接引用要等到运行的时候才能确定!所以针对解析和分派有以下几种指令:

invokestatic:调用静态方法。
invokespecial:调用实例构造器<init>方法、私有方法和父类方法。

invokevirtual:调用所有的虚方法。
invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

其中前两种在解析阶段就能被确定,我们称之为“非虚方法”,如静态方法、私有方法、实例构造器、父类方法。相反其他方法称之为“虚方法”!这里我们讨论的是虚方法!

对于静态分派,我们定义所有依赖静态类型的定位方法的执行版本的分派动作,而典型的应用就是上文提到的重载:静态分派发生在编译阶段,javac在编译的时候根据静态类型决定使用哪个版本,并把这个方法的符号引写到main方法的两条invokevirtual指令的参数中!
对于动态分派,实际上可以分为接口方法分派、继承方法分派。JVM虚拟机在方法分派前会为当前相关类(自身类、接口、父类、子类)生成一个方法表,对于继承父类的方法分派:
JVM 首先查看常量池声明父类Parents的方法表,得出method方法在该方法表中的偏移量 offset,这就是该方法调用的直接引用。当解析出方法调用的直接引用后(方法表偏移量offset),JVM 执行真正的方法调用:根据实例方法调用的参数 this 得到具体的对象(即 继承的某个对象child所指向的位于堆中的对象),据此得到该对象对应的方法表,进而调用方法表中的某个偏移量所指向的方法。

对于接口方法调用,其实现要简单一些:
JVM 首先查看常量池,确定方法调用的符号引用(名称、返回值等等),然后利用 this 指向的实例得到该实例的方法表,进而搜索方法表来找到合适的方法地址。因为每次接口调用都要搜索方法表,所以从效率上来说,接口方法的调用总是慢于类方法的调用的,我们通过把上述“重写”的例子反编译成字节码指令来了解这个过程:
源码:

public static void main(String[] args) {
 Living_beings human = new Human();
    Living_beings dog = new Dog();
    human.run();
    dog.run();
  }

反编译:

public static void main(java.lang.String[]);
    Code:
       0: new           #8                  // class Duotai/Human
       3: dup
       4: invokespecial #9                  // Method Duotai/Human."<init>":()V
       7: astore_1
       8: new           #10                 // class Duotai/Dog
      11: dup
      12: invokespecial #11                 // Method Duotai/Dog."<init>":()V
      15: astore_2
      16: aload_1
      17: invokeinterface #2,  1            // InterfaceMethod Duotai/Living_beings.run:()V
      22: aload_2
      23: invokeinterface #2,  1            // InterfaceMethod Duotai/Living_beings.run:()V
      28: return
}

如果你想知道更为详细的方法表的信息,请参考
java多态实现原理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章