Java 一段代码讲解java的继承、构造函数调用顺和属性覆盖

141 阅读3分钟

前言

在讲解前先来回顾下java jvm相关的基础知识以便好理解。

类加载过程

类加载是指将类的.class文件中的二进制数据读入到内存中,放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构 类的加载分为以下几个主要阶段:

  1. 加载(Loading)

    • 查找并加载类的二进制数据。
    • 一般是指将.class文件读入内存,并为之创建一个java.lang.Class对象。
  2. 链接(Linking)

    • 验证:确保被加载类的正确性。
    • 准备:为类的静态变量分配内存,并将其初始化为默认值。
    • 解析:将类中的符号引用转换为直接引用。
  3. 初始化(Initialization)

    • 为类的静态变量赋予正确的初始值。
    • 声明类变量是指定初始值

类加载的触发条件主要有以下几种:

  • 创建类的实例。
  • 访问类的静态方法。
  • 使用类或接口的静态字段。
  • 反射(如Class.forName("com.example.MyClass"))。
  • 初始化一个类的子类(首先会初始化父类)。
  • Java虚拟机启动时被标明为启动类的类(包含main方法的那个类)。

类加载器有以下几种类型:

  1. 引导类加载器(Bootstrap Class Loader)

    • 加载Java的核心库(JAVA_HOME/jre/lib/rt.jar中的类或者-Xbootclasspath参数指定的路径中的类)。
  2. 扩展类加载器(Extension Class Loader)

    • 加载JAVA_HOME/jre/lib/ext目录中或者由java.ext.dirs系统变量指定的路径中的类库。
  3. 系统类加载器(System/Application Class Loader)

    • 加载系统类路径(Classpath)上指定的类库。

父子类调用顺序

  1. 静态代码块和静态字段初始化(按照声明顺序):

    • 首先执行父类的静态代码块和静态字段初始化,然后执行子类的静态代码块和静态字段初始化。
  2. 父类的成员变量和实例初始化块(按照声明顺序):

    • 当创建子类的实例时,首先初始化父类的成员变量和实例初始化块。
  3. 父类的构造器

    • 在父类的成员变量和实例初始化块初始化之后,调用父类的构造器。
  4. 子类的成员变量和实例初始化块(按照声明顺序):

    • 父类的构造器完成后,初始化子类的成员变量和实例初始化块。
  5. 子类的构造器

    • 在子类的成员变量和实例初始化块初始化之后,调用子类的构造器。

一段代码深入回顾 jvm的执行顺序

/**   Java 一段代码讲解java的继承、构造函数调用顺和属性覆盖
 * @Author: andy
 * @Date: 2024/7/11 10:15
 * @Description:
 **/
public class JVMSsubLClass {
    static class Father {
        public int i = 1;
        public Father() {
            Class<? extends Father> clz = this.getClass();
            i = 2;
            show();
        }
        public void show() {System.out.println("Father i =" + i);}
    }
    static class Son extends Father {
        public int i = 3;
        public Son(){
            i =4;
            show();
        }
        @Override
        public void show() {
            System.out.println("Son i =" + i);
            System.out.println("Son super.i =" + super.i);
        }
    }
    public static void main(String[] args) {
        Father f =new Son();
        System.out.println(f.i);
    }

}

输出结果

Son i =0
Son i =4
2

  1. main 方法中创建 Son 类的实例。
  2. 在创建 Son 的实例之前,首先需要调用父类 Father 的构造方法。
  3. Father 的构造方法设置 i 的值为2。
  4. 然后 Father 的构造方法调用 show 方法。由于 show 方法在 Son 类中被覆盖,因此实际上调用的是 Son 类的 show 方法。
  5. Son 类的 show 方法尝试打印 i 的值。但由于 Son 类的构造方法还没有执行,i 的值还是其默认值0。
  6. Father 的构造方法完成后,Son 的构造方法开始执行。
  7. Son 的构造方法设置 i 的值为4,并再次调用 show 方法。
  8. 这一次,Son 类的 show 方法打印的是 i 的当前值4,以及继承自 Father 类的 i 值2。
  9. 最后,当在 main 方法中打印 f.i 时,由于 f 是以 Father 类型引用的 Son 对象,所以它访问的是 Father 类中定义的 i 字段,其值为2。