Android 类初始化与方法调用

发布于 2024-09-08 07:50:47 字数 4418 浏览 8 评论 0

静态分派

所有依赖静态类型来定位方法执行版本的分派称之为静态分派,它在重载的时候是通过参数的静态类型而不是实际类型作为判断依据的

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, man");
    }

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


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

        StaticDispatch sr = new StaticDispatch();
        sr.sayHello(man);
        sr.sayHello(woman);
    }
}

以 Human man = new Man() 为例,Human 称之为变量的静态类型,后面的 Man 称之为变量的实际类型。区别是:

  • 静态类型的变化发生在编译时期
  • 实际类型的变化发送在运行时

编译器在编译时期不知道变量的实际类型是什么

//实际类型变化
Human man = new Man();
man = new Woman();

//静态类型变化
sr.sayHello((Man)man);
sr.sayHello((Woman)man);

代码中刻意定义了两个静态类型相同但实际类型不同的变量,但编译器在 重载时是通过参数的静态类型作为判断依据的 ,因为静态类型在编译时期可知

动态分派

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


    static class Man extends Human{

        @Override
        protected void sayHello() {
            System.out.println("hello, man");
        }
        
        void hi(){}
    }

    static class Woman extends Human{

        @Override
        protected void sayHello() {
            System.out.println("hello, woman");
        }
        
        void hi(){}
    }


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

        man.sayHello();
        woman.sayHello();

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

显然这里不能再根据静态类型判断了,因为 man 和 woman 的静态类型都是 human 而且在执行 sayHello 时发生了不同的行为,原因在于两个变量的实际类型不一样。

在机器指令中:

  • 首先创建这两个对象引用
  • 然后把引用压到栈顶,他们是 sayHello 的所有者也称之为 接受者
  • 之后 invokevirtual 进行解析:
    • 找到栈顶元素锁指向对象的实际类型,记作 C
    • 如果 C 中找到了相符合的方法则进行权限校验(public,private)
    • 否则,向父类寻找,直到 Object 类
    • 如果还没找到,throw AbstractMethodError 异常

由于 invokevirtual 指令执行的第一步就是在运行期确切的接受者的实际类型,所以两次调用的 invokevirtual 指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程是 java 方法重写的本质 。我们把这种在运行期根据实际类型确定方法执行版本的分派过程称之为动态分派。

方法表

这个数据结构,便是 Java 虚拟机实现动态绑定的关键所在。下面将以 invokevirtual 所使用的虚方法表(virtual method table,vtable)为例介绍方法表的用法。

我们知道,方法调用指令中的符号引用会在执行之前解析成实际引用。对于静态绑定的方法调用而言,实际引用将指向具体的目标方法。对于动态绑定的方法调用而言,实际引用则是方法表的索引值(实际上并不仅是索引值)。在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。

以 DynamicDispatch 为例:

Human

0sayHello

Man

0Man.sayHello
1hi

Woman

0Woman .sayHello
1hi

总结:Java 虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法表中被重写的方法的索引值一致。在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。

栈帧

栈帧里的结构:

  • 局部变量表
  • 操作栈
  • 动态连接
  • 返回地址

局部变量表

public static void main(String args[]){
    int a;
    System.out.println(a);
}

public static void main(String args[]){
    int a = 9;
    change(a);
    System.out.println(a);
}

private static void change(int a) {
    a = 10;
}

他们都输出什么?

类变量有两次赋值,一次是准备阶段系统赋初始值,第二次是程序员赋值;但是局部变量没有准备阶段,因此没有赋值是不能使用的

操作栈

字节码指令出栈入栈,比如做算术运算

动态连接

每个栈帧都包含一个指向运行时常量池该栈帧所属方法的引用

返回地址

方法退出等同于把当前栈帧出栈,然后恢复上层方法,两种方式:正常 return、异常

方法调用字节码指令

  • invokestatic:调用静态方法
  • invokespecial:调用实例构造器方法、父类方法、私有方法
  • invokevirtual:调用虚方法。也包括 final 方法,但 final 方法不是虚方法
  • invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象
  • invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

帥小哥

暂无简介

0 文章
0 评论
22 人气
更多

推荐作者

内心激荡

文章 0 评论 0

JSmiles

文章 0 评论 0

左秋

文章 0 评论 0

迪街小绵羊

文章 0 评论 0

瞳孔里扚悲伤

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文