一步搞清楚多态与类初始化的底层原理

首先我们先看一个段非常有代表性的代码,里面一口气牵扯到了多态和类初始化顺序知识。

public class Test {

    public static void main(String[] args) {
        A test = new B();
    }
}

class A {
    int value = 10;

    A() {
        System.out.println("父类构造器");
        process();
    }

    public void process() {
        System.out.println("父类的process");
        value++;
        System.out.println(value);
    }
}

class B extends A {
    int value = 12;
    {
        value++;
    }
    B() {
        System.out.println("子类构造器");
        process();
    }

    public void process() {
        System.out.println("子类的process");
        System.out.println(value);
        value++;
        System.out.println(value);
    }
}

它的输出是:

父类构造器

子类的process

0

1

子类构造器

子类的process

13

14

我想现在你一定很困惑,不要慌上车!带你了解底层的原理

为什么会调用子类的process()方法?

这里的底层原理是Java的动态分派机制

对于方法重写,Java采用的是动态分派机制,也就是说在运行的时候才确定调用哪个方法。由于A的实际类型是B,因此调用的就是B的process()法。

原理在底层字节码中的invokevirtual指令的多态查找过程,分为以下几个步骤:

  1. 找到操作数栈栈顶的第一个元素所指向的对象的实际类型,记为C
  2. 如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限的校验,如果通过则返回这个方法的直接引用,查找结束;如果不通过,则返回非法访问异常
  3. 如果在类型C中没有找到,则按照继承关系从下到上依次对C的各个父类进行第2步的搜索和验证过程
  4. 如果始终没有找到合适的方法,则抛出抽象方法错误的异常

从这个过程可以发现,在第一步的时候就在运行期确定接收对象(执行方法的所有者程称为接受者)的实际类型,所以当调用invokevirtual指令就会把运行时常量池中符号引用解析为直接引用,这就是方法重写的本质。

相信到这你还是迷迷糊糊的,那是因为缺少对类加载过程中解析知识的了解

解析是类加载的过程之一

解析阶段时虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的地位到目标即可。
  • 直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
  • 我们知道Class文件的常量池中存有大量的符号引用(字节码中方法调用指令就以常量池中指向方法的符号引用作为参数)。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化称为静态解析。
  • 另一部分将在每一次运行期间转化为直接引用,这部分称为动态连接。

通俗点说,所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析能成立的前提是——>方法在程序真正运行之前就有一个可确定的调用版本(主要是静态方法和私有方法),它们的调用版本在运行期是不可变的。因为静态方法和私有方法不可能通过继承或别的方式重写成其他版本!!划重点——>其他版本,因此他们都在类加载阶段解析完成了。

综上可知,在动态分派的机制下,因为子类继承父类重写了process()方法,只有在程序运行时才能确定的调用版本,将符号引用转化成了直接引用,指向了实例的process()方法。

这种在运行期根据实际类型确定方法执版本的分派过程就是动态分派。

为什么打印出来的是0和1?

这是因为在对象实例化的时候,划分内存后会直接赋零值。

对象的创建

  • 虚拟机遇到一条new指令时,首先将会去检查这个指令的参数能否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行相应的类加载过程。
  • 在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
    • 如果Java堆中的内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种叫做指针碰撞
    • 如果Java堆中的内存不是规整的,虚拟机就必须维护一个列表,记录哪块内存块是可用的,在分配的时候从列表中到找一块足够大的空间划分给对象实例,并更新列表上的记录,这钟叫做空闲列表
  • 并发分配对象内存有两种解决方案->方案一:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;方案二:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation Buffer,TLAB)。哪个线程要分配内存就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。是否开启TLAB:-XX:+/-UseTLAB
  • 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值,这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  • 虚拟机要对对象进行设置,例如对象是哪个实例,如何找到类的元数据信息、对象的哈希码、对象GC分代年龄,将这些信息存放在对象头之中。
  • 知执行new指令之后会接着执行init方法,把对象按照程序员的意愿进行初始化。

可以知道,当父类调用子类的process()方法时,子类并没有初始化完成,仅仅是分配了内存,这里有个实例变量初始化顺序:

遵循的原则是:

  (1)按照代码中的顺序依次执行实例变量定义语句和实例变量代码块;

  (2)如果创建该类的对象时该类的类变量尚未初始化,则先初始化类变量,再初始化实例变量;

  (3)如果该类有父类的话,则先创建一个父类对象;并且,如果父类类变量没被初始化时,先初始化父类的类变量,再初始化父类的实例变量,再调用父类的默认构造器;

//有继承的情形(且该类和父类的类变量未被初始化)
1.父类的static变量初始化和static代码块
2.子类的static变量初始化和static代码块
3.父类的实例变量初始化和实例变量初始化代码块
4.父类的构造函数
5.子类的实例变量初始化和实例变量初始化代码块
6.子类构造函数

相信到这你理解了为什么会打出0和1了,是因为父类的构造函数是在子类的实例变量初始化之前执行的。所以当输出value时,其值为0。

原文地址:https://www.cnblogs.com/keeya/p/9380107.html

时间: 2024-08-07 12:31:31

一步搞清楚多态与类初始化的底层原理的相关文章

C++——类继承以及类初始化顺序

对于类以及类继承, 几个主要的问题:1) 继承方式: public/protected/private继承. 这是c++搞的, 实际上继承方式是一种允许子类控制的思想. 子类通过public继承, 可以把基类真实还原, 而private继承则完全把基类屏蔽掉. 这种屏蔽是相对于对象层而言的, 就是说子类的对象完全看不到基类的方法, 如果继承方式是private的话, 即使方法在基类中为public的方法. 但继承方式并不影响垂直方向的访问特性, 那就是子类的函数对基类的成员访问是不受继承方式的影

Java 继承、多态与类的复用

摘要: 本文结合Java的类的复用对面向对象两大特征继承和多态进行了全面的介绍. 首先,我们介绍了继承的实质和意义,并探讨了继承,组合和代理在类的复用方面的异同.紧接着,我们依据继承引入了多态.介绍了它的实现机制和详细应用.此外,为了更好地理解继承和多态.我们对final关键字进行了全面的介绍. 在此基础上.我们介绍了Java中类的载入及初始化顺序.最后.我们对面向对象设计中三个十分重要的概念–重载.覆盖与隐藏进行了详细的说明. 要点: 继承 组合,继承,代理 多态 final 关键字 类载入及

[读书笔记] java类初始化

以下内容来自周志明的<深入理解java虚拟机>: 类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制. 到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码). 在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程.

iOS开发三步搞定百度推送

iOS开发三步搞定百度推送 百度推送很简单,准备工作:在百度云推送平台注册应用,上传证书. 步骤一: 百度云推送平台 http://push.baidu.com/sdk/push_client_sdk_for_ios  在这里下载iOS端SDK包,如下图: 把SDK包里面的下图文件夹拖到你的工程中,如下图,第一步就这么简单. 步骤二: 在工程中AppDelegate.m中的- (BOOL)application:(UIApplication *)application didFinishLaun

C++与C#对比学习:类初始化

类和柏拉图的理念世界 我们知道面向对象编程中到处是一个个的类,但类只是个概念性的东西,不是个实体,不占内存,你没实例化之前也不能用它.只有把类实例化成一个对象后,它才是一个真正存在的实体.占有内存,能被我们使用.类就有点像柏拉图所说的理念世界一样,柏拉图认为存在着两个世界,一个是我们生活于其中的现实世界,与之对立的是理念世界,理念世界有些啥东东呢?那是个永恒不变的世界,由一堆堆的理念组成,比如人,马,鸟....理念的人只是个概念,不是任何实际的人.而我们现实世界是根据理念世界模拟出来实际的一个个

java中的类加载器ClassLoader和类初始化

每个类编译后产生一个Class对象,存储在.class文件中,JVM使用类加载器(Class Loader)来加载类的字节码文件(.class),类加载器实质上是一条类加载器链,一般的,我们只会用到一个原生的类加载器AppClassLoader,它只加载Java API等可信类,通常只是在本地磁盘中加载,这些类一般就够我们使用了.如果我们需要从远程网络或数据库中下载.class字节码文件,那就需要我们来挂载额外的类加载器. 一般来说,类加载器是按照树形的层次结构组织的,每个加载器都有一个父类加载

类初始化和实例初始化

一.类初始化过程 一个类要创建实例需要先加载并初始化该类 main方法所在的类需要先加载和初始化 一个子类要初始化需要先初始化父类 一个类初始化就是执行< clinit>()方法 < clinit>()方法由静态类变量显示赋值代码和静态代码块组成 类变量显示赋值代码和静态代码块代码从上到下顺序执行 < clinit>()方法只执行一次 二.实例初始化过程 实例初始化就是执行()方法 < init>()方法可能重载有多个,有几个构造器就有几个方法 < i

第六篇:为多态基类声明虚析构函数

前言 在很多类中,可以看到其析构函数都是声明为虚函数的. 那么,为何要将析构函数声明为虚函数?哪些情况要将析构函数声明为虚函数? 本文将为你解惑. 在使用 C++ 实现多态的时候,有一点一定要清楚:当派生类对象经由基类指针被删除,而此基类的析构函数没有被声明为虚函数的话,那么析构函数只会释放基类部分的成员而无视派生类成员. 如果不对这一点加以防范,那么很多时候,会带来内存泄露这样灾难性的后果. 问题描述 假设,有以下几个类,分别代表:钟,原子钟,水钟,腕表: 1 // 钟 2 class Tim

redha7.2配置静态网卡(三步搞定)

三步搞定: 编辑网卡配置文件:vi /etc/sysconfig/network-scripts/ifcfg-eno16777736(也可能是eth0或eth1或者eth+任何数字) 当前是DHCP,如果要使用静态IP修改: BOOTPROTO=static                #声明IP地址获得的途径 ONBOOT=yes                          #开启自动启用网络连接 增加: IPADDR=192.168.1.1              #设置IP地址(