banner
RustyNail

RustyNail

coder. 【blog】https://rustynail.me 【nostr】wss://ts.relays.world/ wss://relays.world/nostr

Java多态和Jvm的分派及其他

重写的实现和动态分派(Dispatch)有紧密的联系,分派的大概意思是,指定调用多个实现中的某个实现。

动态分派#

比如说

public class DynamicDispatch {

    static abstract class Father {
        public abstract void print();
    }

    static class Son extends Father {
        public void print() {
            System.out.println("son");
        }
    }

    static class Daughter extends Father {
        public void print() {
            System.out.println("Daughter");
        }
    }
    
    public static void main(String[] args) {
        Father son = new Son();
        Father dou = new Daughter();

        son.print();
        dou.print();
    }
}

当然,我们完全知道,会输出

son
Daughter

但是,在表现类型为 Father 的情况下,他是怎么知道 son 对应的 print 是输出son的那个呢?

javap 输出字节码:

Classfile /D:/tech/java/vmdemo/src/main/java/DynamicDispatch.class
  Last modified 2019-1-10; size 499 bytes
  MD5 checksum a5660428b8d1d4ed7fb4f04bd86f7738
  Compiled from "DynamicDispatch.java"
public class DynamicDispatch
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #8.#22         // java/lang/Object."<init>":()V
   #2 = Class              #23            // DynamicDispatch$Son
   #3 = Methodref          #2.#22         // DynamicDispatch$Son."<init>":()V
   #4 = Class              #24            // DynamicDispatch$Daughter
   #5 = Methodref          #4.#22         // DynamicDispatch$Daughter."<init>":()V
   #6 = Methodref          #12.#25        // DynamicDispatch$Father.print:()V
   #7 = Class              #26            // DynamicDispatch
   #8 = Class              #27            // java/lang/Object
   #9 = Utf8               Daughter
  #10 = Utf8               InnerClasses
  #11 = Utf8               Son
  #12 = Class              #28            // DynamicDispatch$Father
  #13 = Utf8               Father
  #14 = Utf8               <init>
  #15 = Utf8               ()V
  #16 = Utf8               Code
  #17 = Utf8               LineNumberTable
  #18 = Utf8               main
  #19 = Utf8               ([Ljava/lang/String;)V
  #20 = Utf8               SourceFile
  #21 = Utf8               DynamicDispatch.java
  #22 = NameAndType        #14:#15        // "<init>":()V
  #23 = Utf8               DynamicDispatch$Son
  #24 = Utf8               DynamicDispatch$Daughter
  #25 = NameAndType        #29:#15        // print:()V
  #26 = Utf8               DynamicDispatch
  #27 = Utf8               java/lang/Object
  #28 = Utf8               DynamicDispatch$Father
  #29 = Utf8               print
{
  public DynamicDispatch();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class DynamicDispatch$Son
         3: dup
         4: invokespecial #3                  // Method DynamicDispatch$Son."<init>":()V
         7: astore_1
         8: new           #4                  // class DynamicDispatch$Daughter
        11: dup
        12: invokespecial #5                  // Method DynamicDispatch$Daughter."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method DynamicDispatch$Father.print:()V
        20: aload_2
        21: invokevirtual #6                  // Method DynamicDispatch$Father.print:()V
        24: return
      LineNumberTable:
        line 24: 0
        line 25: 8
        line 27: 16
        line 28: 20
        line 29: 24
}
SourceFile: "DynamicDispatch.java"
InnerClasses:
     static #9= #4 of #7; //Daughter=class DynamicDispatch$Daughter of class DynamicDispatch
     static #11= #2 of #7; //Son=class DynamicDispatch$Son of class DynamicDispatch
     static abstract #13= #12 of #7; //Father=class DynamicDispatch$Father of class DynamicDispatch

0-15 实在 new 那两个对象,和初始化,然后把应用存在 local 变量里边:[son,doughter]

然后,先拿出变量位置为1的对象引用 (son),放到栈顶,调用invokevirtual,#6是个方法,invokevirtual 指令自动找到当前对象里

DynamicDispatch$Father.print:()V 的方法,然后调用,也就是调用了son.print();

invokevirtual#

整个过程的关键是invokevirtual,invokevirtual 是如何查找对应的方法的呢?

  1. 找到操作数栈栈顶元素,拿到该元素所指的对象类型 C
  2. 在 C 中寻找符合要求的方法,如果找到了,就进行访问权限校验,通过了就进行返回,不通过抛IllegalAccessError
  3. 如果没找到,根据继承关系,向父类进行查找
  4. 最后还是没找到就抛AbstractMethodError

那为什么字节码是先aload_1的呢?因为字节码是编译后就确定的了,所以只能是编译器这边做的工作,大概是类型推断之类的(不确定)

运行时进行确定实际类型、确定执行的方法版本的过程叫动态分派。

这就是 jvm 对动态分派的处理,也就是实现了 java 的重写,程序员借助重写编写多态程序。

静态分派#

静态分派就没这么复杂,对应的是方法重载,(有人说方法重载不算多态的表现形式,因为编译完就确定了要使用哪一个方法)

public class StaticDispatch {

    static void print(int i){
        System.out.println("int!");
    }

    static void print(char i){
        System.out.println("char!");
    }

    static void print(boolean i){
        System.out.println("bool!");
    }

    public static void main(String[] args) {
        print(1);
        print('1');
        print(true);
    }
}

方法重载调用的是尽可能相近的方法,比如假如没有print(char)而调用print('a')的时候,会给你调用print(int)

上边的代码的字节码是这样的:

{
  public StaticDispatch();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 5: 0

  static void print(int);
    descriptor: (I)V
    flags: ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String int!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8

  static void print(char);
    descriptor: (C)V
    flags: ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String char!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 12: 0
        line 13: 8

  static void print(boolean);
    descriptor: (Z)V
    flags: ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String bool!
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 16: 0
        line 17: 8

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iconst_1
         1: invokestatic  #7                  // Method print:(I)V
         4: bipush        49
         6: invokestatic  #8                  // Method print:(C)V
         9: iconst_1
        10: invokestatic  #9                  // Method print:(Z)V
        13: return
      LineNumberTable:
        line 20: 0
        line 22: 4
        line 24: 9
        line 25: 13
}

编译完了就给你排好要调用哪个方法了。

单分派、多分派#

宗量, 个人理解是方法的接收者和方法的参数叫方法的宗量

  • 根据宗量的大小可以区分是单分派还是多分派

比如一下代码,在分派的时候有 doRead 两个宗量 (参数不同)

public class Book{
  public void read(){}

  public static void doRead(Book b){
    b.read();
  }

  public static void doRead(Picture p){
    p.read();
  }
}

所以说 java 是静态多分派的语言,而在动态分派的时候总是在找栈顶引用对象里的那一个方法(也就是说参数不变的),所以动态分派的时候是

单分派的。Java 是一门静态多分派,动态单分派的语言

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.