Java JVM analyzes the essence of overloading and rewriting from the perspective of method call

Keywords: Java jvm github

Original source of column: github - source note file github source code , welcome Star, please attach the original source link and this statement.

Java JVM - virtual machine column series notes, system learning can access personal copy notes - Technology Blog Java JVM virtual machine

I. Preface

Method call does not mean that the code in the method is executed. All stored in the Class file is just a symbol reference.

This feature brings more powerful dynamic extension ability to Java. Some calls need to be made during class loading and some can only determine the direct reference of the target method at runtime.

This section analyzes the implementation process of overloading and rewriting in Java based on bytecode.

2, Method call bytecode instruction

The Java virtual machine supports the following five methods to call bytecode instructions

  • invokestatic: used to call static methods

  • invokespecial: used to call methods in instance constructor < init > () method, private method and parent class

  • invokevirtual: used to call all virtual methods.

  • invokeinterface: used to call interface methods, and another object implementing the interface will be determined at runtime

  • Invokedynamic: dynamically resolve the method referenced by the call point qualifier at run time before executing the method. In the first four instructions, the dispatch logic is fixed in the Java virtual machine, and the dispatch logic of invokedynamic instructions is determined by the guidance method set by the user.

3, Non virtual method and virtual method

  1. Non virtual method

There are four kinds of methods: static method, private method, instance constructor and parent class method, plus the method modified by final (although it is called by invokevirtual instruction), they can resolve the symbol reference to the direct reference of the method when the class is loaded.

  1. Virtual method

The opposite of the non virtual method. The version of the specific method can only be determined during runtime.

4, Static dispatch and overload

public class StaticDispatch {
    
    interface Human { }
    static class Man implements Human { }
    static class Woman implements Human { }

    public void sayHello(Human guy) {
        System.out.println("Human sayHello");
    }

    public void sayHello(Man guy) {
        System.out.println("Man sayHello");
    }

    public static void main(String[] args) {
        final Human human = new Man();
        final StaticDispatch dispatch = new StaticDispatch();

        dispatch.sayHello(human); // Human sayHello
        dispatch.sayHello((Man) human); // Man sayHello
    }
}

For the above code, we call Human "static type" and Man/Woman "runtime type"

  • Static types are known during compilation
  • The runtime type can only be determined during runtime. The compiler does not know what type human is during compilation
  • The runtime type can be cast to parent at runtime, so the type changes.

Therefore, during compilation, when a method is overloaded, which method is called depends on the static type of the method parameter.

Part of the bytecode of the main method is as follows:

 17: aload_1
 18: invokevirtual #10 / / make sure to call sayhello: (lstatic dispatch $human;) V
 21: aload_2
 22: aload_1
 23: checkcast     #6. Static dispatch $man
 26: invokevirtual #11 / / make sure to call sayhello: (lstatic dispatch $man;) V
 29: return

Conclusion:

During the compilation phase, the dispatch process that depends on the static type to determine the execution version of the method is called "static dispatch". The most typical application of static dispatch is method overloading.

Be careful:

If we annotate the sayHello(Man guy) method, the program can still compile and run. Finally, all the two methods call sayHello(Human guy), because the overloaded version is not unique. In this fuzzy situation, the compiler will choose a more appropriate version.
We should avoid this in actual coding.

5, Dynamic dispatch and rewrite

public class DynamicDispatch {
    
    interface Human { 
        void sayHello();
    }

    static class Man implements Human {
        @Override public void sayHello() {
            System.out.println("Man sayHello");
        }
    }

    static class Woman implements Human {
        @Override public void sayHello() {
            System.out.println("Woman sayHello");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        final Human woman = new Woman();

        man.sayHello(); // Man sayHello
        woman.sayHello(); // Woman sayHello

        man = new Woman();
        man.sayHello(); // Woman sayHello
    }
}

Part of the bytecode of the main method is as follows:

 15: astore_2
 16: aload_1
 17: invokeinterface #6,1 // InterfaceMethod DynamicDispatch$Human.sayHello:()V
 22: aload_2
 23: invokeinterface #6,1 // InterfaceMethod DynamicDispatch$Human.sayHello:()V
 28: new             #4   // class DynamicDispatch$Woman
 31: dup
 32: invokespecial   #5   // Method DynamicDispatch$Woman."<init>":()V
 35: astore_1
 36: aload_1
 37: invokeinterface #6,1 // InterfaceMethod DynamicDispatch$Human.sayHello:()V
 42: return

Analyzing the invokeinterface instruction in bytecode is the key to determine the method call version.

For the invokeinterface instruction execution process, suppose C is of ref type, and the method actually called is found according to the following procedure:

  1. If C contains a method declaration that is the same as the name and descriptor of the method to be resolved, the declaration is the method to be called, and the search ends.

  2. Otherwise, if C has a parent class, the search in step 1 is performed recursively in its parent class and its parent class.

  3. If not found, an AbstractMethodError is thrown.

Conclusion:

We call this dispatch process of determining the execution version of the method according to the actual type at runtime "dynamic dispatch". This process is the essence of method rewriting.

6, Re understanding rewriting overload for a complex inferior problem

Let's take an example:

  • Fruit interface: fruit has the implementation of Apple and Orange
  • The methods in Father are overloaded
  • Son inherits Father and rewrites the method
public class Dispatch {

    interface Fruit { }

    static class Apple implements Fruit { }

    static class Orange implements Fruit { }

    static class Father {

        public void eat(Fruit o) {
            System.out.println("Father.Fruit");
        }

        public void eat(Apple o) {
            System.out.println("Father.Apple");
        }

        public void eat(Orange o) {
            System.out.println("Father.Orange");
        }
    }

    static class Son extends Father {
        public void eat(Fruit o) {
            System.out.println("Son.Fruit");
        }

        @Override public void eat(Apple apple) {
            System.out.println("Son.Apple");
        }

        @Override public void eat(Orange apple) {
            System.out.println("Son.Orange");
        }
    }

    public static void main(String[] args) {
        final Fruit fruit = new Apple();
        final Fruit fruitImpl = new Fruit() {};
        final Apple apple = new Apple();
        final Orange orange = new Orange();

        final Father father = new Father();
        final Father son = new Son();

        father.eat(fruit);
        father.eat(fruitImpl);
        father.eat(apple);

        son.eat(fruit);
        son.eat(fruitImpl);
        son.eat(orange);
    }
}

In the face of the six methods finally called, our analysis ideas are as follows:

  • At compile time, which class calls which overloaded method static dispatch procedure.
    Because static dispatch mainly refers to "static type", we can get a normal static type. Eat (static type o)

  • During operation, which version of the method to execute belongs to the dynamic dispatch process.
    Dynamic dispatch looks up according to the actual type of runtime. When son calls, it first looks for the method declaration with the same name and descriptor as the resolved method. If it is not looking up from the parent class, other.

The final answer is:

Father.Fruit
Father.Fruit
Father.Apple
Son.Fruit
Son.Fruit
Son.Orange

summary

  • Non virtual method: directly referenced methods (static method, private method, instance constructor, parent method, final modified method) can be resolved during class loading

  • Non virtual method: the method of executing version can only be determined during operation

  • Static dispatch: in the compilation phase, the static type is used to determine the dispatch process of the execution version of the method

  • Static dispatch: the most typical application is method overload

  • Dynamic dispatch: during operation, the dispatch process of the version is executed according to the actual type determination method

  • Dynamic dispatch: the most typical application is method rewrite

All source codes of this content can be referred to github

Reference resources

More notes on this column

230 original articles published, 54 praised, 370000 visitors+
His message board follow

Posted by bruceleejr on Wed, 15 Jan 2020 04:18:10 -0800