方法调用的实现

应用代码经过编译、类加载的各种阶段,进入了JVM的运行时数据区。代码的执行其实本质上是方法的执行,站在JVM的角度归根到底还是字节码的执行。


一、解析

关于方法的调用,Java 字节码共提供了5个指令,来调用不同类型的方法:

  1. invokestatic: 用来调用静态方法
  2. invokespecial: 用于调用私有方法、构造器及 super 关键字等
  3. invokevirtual: 用于调用所有虚方法;如非私有实例方法 public 和 protected,大多数方法调用属于这一种
  4. invokeinterface: 调用接口方法
  5. invokedynamic: 调用动态方法

1.1、非虚方法

如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法


只要能被invokestaticinvokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,满足该条件的方法有静态方法、私有方法、实例构造器、父类方法 4 种,再加上被 final 修饰的方法(尽管它使用 invokevirtual 指令调用),这 5 种方法调用会在类加载的时候就可以把符号引用解析为该方法的直接引用。不需要在运行时再去完成。


1.2、虚方法

与非虚方法相反,不是非虚方法的方法就是虚方法。主要包括以下字节码中的两类:

  • invokevirtual 用于调用非私有实例方法,比如 public 和 protected,大多数方法调用属于这一种(排除掉被 final 修饰的方法)
  • invokeinterface 和上面这条指令类似,不过作用于接口类

为什么叫做虚方法呢?就是方法在运行时是可变的。

很多时候,JVM需要根据调用者的动态类型,来确定调用的目标方法,这就是动态绑定的过程;相对比,invokestatic 指令加上 invokespecial 指令,就属于静态绑定过程。


二、分派

Java 是一门面向对象的程序语言,因为Java具备面向对象的3个基本特征:继承、封装和多态

分派调用过程将会揭示多态性特征的一些最基本的体现,如“重载”和“重写”在 Java 虚拟机之中是如何实现的


2.1、静态分派

多见于方法的重载,如下:

public class StaticDispatch {

    static abstract class Human {}

    static class Man extends Human {}

    static class Woman extends Human {}

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

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

    public void sayHello(Woman guy) {
        System.out.println("Hello, Lady");
    }

    public static void main(String[] args) {
        Human h1 = new Man();
        Human h2 = new Woman();

        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(h1);
        sr.sayHello(h2);
    }
}

在这里插入图片描述

“Human” 称为变量的静态类型(Static Type),或者叫做的外观类型(Apparent Type),后面的 “Man” 则称为变量的实际类型(Actual Type)。

静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的;而实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。


代码中定义了两个静态类型相同但实际类型不同的变量,但虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。并且静态类型是编译期可知的,因此在编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本,所以选择 sayHello(Human) 作为调用目标。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。

静态分派的典型应用是方法重载。静态分派发生在编译阶段,因此确定静态分派的动作实际上不是由虚拟机来执行的。


2.2、动态分派

多见于方法的重写,如下:

public class DynamicDispatch {
    static abstract class Human {
        protected abstract void sayHello();
    }

    static class Man extends Human {
        @Override
        protected void sayHello() {
            System.out.println("Hello, Gentleman");
        }
    }

    static class Woman extends Human {
        @Override
        protected void sayHello() {
            System.out.println("Hello, Lady");
        }
    }

    public static void main(String[] args) {
        Human h1 = new Man();
        Human h2 = new Woman();
        h1.sayHello();
        h2.sayHello();
    }
}

在这里插入图片描述

静态类型同样都是 Human 的两个变量 man 和 woman 在调用 sayHello() 方法时执行了不同的行为,并且在两次调用中执行了不同的方法。导致这个现象的原因很明显,是这两个变量的实际类型不同。


在实现上,最常用的手段就是为类在方法区中建立一个虚方法表。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表里面的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类方法表中的地址将会替换为指向子类实现版本的入口地址。
在这里插入图片描述
如图中,Son重写了来自Father的方法,因此Son的方法表没有指向Father类型数据的箭头。但是Son和Father都没有重写来自Object的方法,所以它们的方法表中所有从Object继承来的方法都指向了Object的数据类型。


三、Lambda表达式

invokedynamic通常在 Lambda 语法中出现,invokedynamic指令的底层,是使用方法句柄MethodHandle来实现的。


方法句柄是一个能够被执行的引用,它可以指向静态方法和实例方法,以及虚构的get和set方法。简单来说可以通过方法句柄来调用相应的方法。

3.1、MethodHandle 方法句柄

用 MethodHandle 调用方法的流程为:

  1. 创建 MethodType,获取指定方法的签名(出参和入参)
  2. 在 Lookup 中查找 MethodType 的方法句柄 MethodHandle
  3. 传入方法参数通过 MethodHandle 调用方法
public class MethodHandleDemo {
    static class Dog {
        protected String sayHello() {
            return "Woof Woof...";
        }
    }

    static class Human {
        protected String sayHello() {
            return "Hello, Guy";
        }
    }

    static class Man extends Human {
        @Override
        protected String sayHello() {
            return "Hello, Gentleman";
        }
    }

    static String sayHello(Object obj) throws Throwable {
        // 从工厂方法中获取方法句柄
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        // 创建MethodType,获取指定方法的签名(出参和入参)
        MethodType methodType = MethodType.methodType(String.class);
        // 获取具体的MethodHandle
        MethodHandle methodHandle = lookup.findVirtual(obj.getClass(), "sayHello", methodType);
        
        return (String) methodHandle.invoke(obj);
    }

    public static void main(String[] args) throws Throwable {
        System.out.println(MethodHandleDemo.sayHello(new Dog()));
        System.out.println(MethodHandleDemo.sayHello(new Human()));
        System.out.println(MethodHandleDemo.sayHello(new Man()));
    }
}

在这里插入图片描述
Lambda实际上是通过上述方法句柄来完成的,会根据你编写的Lambda表达式的代码,编译出一套可以去调用 MethodHandle 的字节码代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值