天天看点

JVM(复习)方法调用

JVM(复习)方法调用

文章目录

  • ​​JVM(复习)方法调用​​
  • ​​一,方法重载​​
  • ​​二,方法重写​​

一,方法重载

何为静态类型,何为实际类型?
static class GrandFather{

    }

    static class Father extends GrandFather{

    }

    static class Child extends Father{

    }      

上面声明了三个类型:

GrandFather grandFather = new Father();      

grandFather的静态类型是GrandFather,而grandFather的实际类型是真正指向的类型是Father,变量的静态类型是不会发生变化的,而变量的实际类型是可以发生变化的(多态),实际类型在运行期确定

所有依赖静态类型来定位执行哪一个方法的动作就叫做静态分派
public void test(GrandFather grandFather){
        System.out.println("GrandFather");
    }

    public void test(Father Father){
        System.out.println("Father");
    }

    public void test(Child Child){
        System.out.println("Child");
    }

    //方法重载是一种静态分派行为,在编译期可以确定
    public static void main(String[] args) {
        //
        GrandFather father = new Father();
        GrandFather child = new Child();
        Father father1 = new Father();
        Test1 test1 = new Test1();
        test1.test(father); // GrandFather
        test1.test(father1);// Father
        test1.test(child);// GrandFather
    }      

所以,不难得多,根据静态分派规则看静态类型执行相应的重载的方法

JVM(复习)方法调用
重载方法的匹配优先级

在很多情况下,重载方法的版本并不是唯一,选择调用的那个重载方法只是当前情况下最合适的一个而已

public void say(Object arg){
        System.out.println("Object");
    }

    public void say(int arg){
        System.out.println("int");
    }

    public void say(long arg){
        System.out.println("long");
    }

    public void say(char arg){
        System.out.println("char");
    }

    public void say(Character arg){
        System.out.println("Character");
    }

    public void say(char ... arg){
        System.out.println("char ...");
    }

    public void say(Serializable arg){
        System.out.println("Serializable");
    }      

我先这样执行:

public static void main(String[] args) {
        Test1 test1 = new Test1();
        test1.say('a');
    }      

很容易会想到输出char,因为我给的就是char类型

JVM(复习)方法调用

如果我把char类型的方法去掉

public void say(Object arg){
        System.out.println("Object");
    }

    public void say(int arg){
        System.out.println("int");
    }

    public void say(long arg){
        System.out.println("long");
    }

    public void say(Character arg){
        System.out.println("Character");
    }

    public void say(char ... arg){
        System.out.println("char ...");
    }

    public void say(Serializable arg){
        System.out.println("Serializable");
    }      

还是一样的main方法,参数还是’a’

char类型的’a’自动类型转换为int,所以输出int
JVM(复习)方法调用

再把int对应的方法注释:

public void say(Object arg){
        System.out.println("Object");
    }

    public void say(long arg){
        System.out.println("long");
    }

    public void say(Character arg){
        System.out.println("Character");
    }

    public void say(char ... arg){
        System.out.println("char ...");
    }

    public void say(Serializable arg){
        System.out.println("Serializable");
    }      

继续上边的实验

int继续向上转型为long
JVM(复习)方法调用

对于基本类型:

  • char -> int -> long -> float -> double
  • char不能转为short或byte

继续上边实验

public void say(Object arg){
        System.out.println("Object");
    }

    public void say(Character arg){
        System.out.println("Character");
    }

    public void say(char ... arg){
        System.out.println("char ...");
    }

    public void say(Serializable arg){
        System.out.println("Serializable");
    }      
这次char发生了一次自动装箱,char -> Character
JVM(复习)方法调用

继续注释Character

public void say(Object arg){
        System.out.println("Object");
    }

    public void say(char ... arg){
        System.out.println("char ...");
    }

    public void say(Serializable arg){
        System.out.println("Serializable");
    }      
可以看到char自动装箱后找不到包装类型Character,就去找其包装类型实现的接口类型

最后剩下两个方法

public void say(Object arg){
        System.out.println("Object");
    }

    public void say(char ... arg){
        System.out.println("char ...");
    }      
包装类型找不到,包装类实现的接口类型找不到,就找包装类的父类啦

JVM(复习)方法调用

二,方法重写

方法重载时编译期就根据静态类型确定了要调用的方法版本,但是方法重写时在运行期才确定
public class Test2 {

    static class Fruit{
        public void test(){
            System.out.println("Fruit");
        }
    }

    static class Apple extends Fruit{
        @Override
        public void test(){
            System.out.println("Apple");
        }
    }

    static class Banana extends Fruit{
        @Override
        public void test(){
            System.out.println("Banana");
        }
    }

    public static void main(String[] args) {
        Fruit apple = new Apple();
        Fruit banana = new Banana();
        apple.test();//apple
        banana.test();//banana
    }
}      

对于上面的代码jvm字节码指令是这样的

JVM(复习)方法调用

重点看这一段:

(java.lang.String[]);
    Code:
       # new指令在堆上开辟空间来给apple分配内存
       0: new           #2                  // class Test2$Apple
       3: dup
       # 调用apple的构造方法
       4: invokespecial #3                  // Method Test2$Apple."<init>":()V
       # 将构造出来的对象引用存放在main方法局部变量表上 
       7: astore_1
       8: new           #4                  // class Test2$Banana
      11: dup
      12: invokespecial #5                  // Method Test2$Banana."<init>":()V
      15: astore_2
      16: aload_1
      17: invokevirtual #6                  // Method Test2$Fruit.test:()V
      20: aload_2
      21: invokevirtual #6                  // Method Test2$Fruit.test:()V
      24: return      

完成对象构造

= new Apple();      
# new指令在堆上开辟空间来给apple分配内存
   0: new           #2                  // class Test2$Apple
   # 将apple引用复制一份放到操作数栈栈顶
   3: dup
   # 调用apple的构造方法
   4: invokespecial #3                  // Method Test2$Apple."<init>":()V
   # 将构造出来的对象引用存放在main方法局部变量表上      

看方法的调用

apple.test();//apple      

字节码指令:

#6                  // Method Test2$Fruit.test:()V      
从字节码注释中可以看到执行的是Fruit的test方法,但是实际上是执行Apple的test方法才对,所以这是为什么呢?

且两个不同调用者的invokevirtual参数一样

17: invokevirtual #6                  // Method Test2$Fruit.test:()V
      20: aload_2
      21: invokevirtual #6                  // Method Test2$Fruit.test:()V      

我们需要分析invokevirtual指令的执行步骤:

  • 找到操作数栈栈顶第一个元素,刚刚也知道操作数栈顶是刚刚new出来的apple的引用,该元素记为C,其实就是确定实际类型的过程
  • 如果在C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限的校验,如果通过则返回该方法的直接引用,查找过程结束,否则按照继承关系从下往上一次查找和验证

    就比如:

#6                  // Method Test2$Fruit.test:()V      

去常量池找索引为6的描述符就是这个 Method Test2$Fruit.test:()V

可以看到他们相同的符号引用,但是却被解析到了不同的直接引用上,这是用为,invokevirtual第一步是在运行期确定方法调用者的实际类型,这也正是方法重写的本质,运行期根据实际类型确定方法执行版本的分派也叫作动态分派