banner
RustyNail

RustyNail

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

Java Polymorphism, JVM Dispatch, and Others

The implementation of overriding is closely related to dynamic dispatch (Dispatch), which generally means specifying the invocation of a certain implementation among multiple implementations.

Dynamic Dispatch#

For example:

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();
    }
}

Of course, we know that it will output:

son
Daughter

However, in the case where the type is Father, how does it know that the print corresponding to son outputs son?

The javap command outputs the bytecode:

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 are for creating those two objects and initializing them, then storing the application in local variables: [son,daughter]

Then, it first takes the object reference at position 1 (son) and puts it on the top of the stack, calling invokevirtual, #6 is a method, and the invokevirtual instruction automatically finds the method named DynamicDispatch$Father.print:()V in the current object, and then calls it, which is son.print();

invokevirtual#

The key to the whole process is invokevirtual. How does invokevirtual find the corresponding method?

  1. Find the top element of the operand stack and get the type C of the object it points to.
  2. Look for the required method in C. If found, perform access permission checks; if passed, return; if not, throw IllegalAccessError.
  3. If not found, search for the method in the parent class based on the inheritance relationship.
  4. If still not found, throw AbstractMethodError.

So why is the bytecode first aload_1? Because the bytecode is determined after compilation, so it can only be the work done by the compiler, which is likely type inference (not certain).

The process of determining the actual type and the version of the method to execute at runtime is called dynamic dispatch.

This is how the JVM handles dynamic dispatch, which implements Java's overriding, allowing programmers to write polymorphic programs through overriding.

Static Dispatch#

Static dispatch is not so complicated; it corresponds to method overloading (some say method overloading is not a form of polymorphism because it is determined which method to use after compilation).

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);
    }
}

Method overloading calls the closest method possible. For example, if there is no print(char) and you call print('a'), it will call print(int) instead.

The bytecode for the above code is as follows:

{
  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
}

After compilation, it arranges which method to call.

Single Dispatch and Multiple Dispatch#

Arity, my personal understanding is that the receiver of the method and the parameters of the method are called the arity of the method.

  • Based on the arity, it can be distinguished whether it is single dispatch or multiple dispatch.

For example, in the following code, there are two arities (different parameters) when dispatching doRead:

public class Book{
  public void read(){}

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

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

Therefore, Java is a statically multi-dispatch language, while during dynamic dispatch, it always looks for the method in the top reference object (meaning the parameters remain unchanged), so dynamic dispatch is single dispatch. Java is a statically multi-dispatch, dynamically single-dispatch language.

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