关于 java 继承的哪些事

本贴最后更新于 1820 天前,其中的信息可能已经时移世改

引言

本文结合一个例子来说明继承实现的基本原理。

基类 Base 代码如下所示:

public class Base {
    public static int s;
    private int a;
    static {
        System.out.println("基类静态代码块,s:"+s);
        s=1;
    }
    {
        System.out.println("基类实例代码块,a:"+a);
        a=1;
    }
    public Base(){
        System.out.println("基类构造方法,a:"+a);
        a=2;
    }
    protected void step(){
        System.out.println("base s:"+s+",a:"+a);
    }
    public void action(){
        System.out.println("start");
        step();
        System.out.println("end");
    }
}

注意: Base 包含一个静态变量 s,一个实例变量 a,一段静态初始化代码块,一段实例初始化代码块,一个构造方法,两个方法 step 和 action。

子类 Child 代码如下所示:

public class Child extends Base{
    public static int s;
    private int a;
    static {
        System.out.println("子类静态代码块,s:"+s);
        s=10;
    }
    {
        System.out.println("子类实例代码块,a:"+a);
        a=10;
    }
    public Child(){
        System.out.println("子类构造方法,a:"+a);
        a=20;
    }

    @Override
    protected void step() {
        System.out.println("child s:"+s+",a:"+a);
    }
}

注意: 子类 Child 继承了 Base,也定义了和基类同名的静态变量 s 和实例变量 a 并且重写了方法 step。

测试的 main 方法代码如下所示:

public static void main(String[] args) {
    System.out.println("------------- new Child()");
    Child c=new Child();
    System.out.println("\n-- c.action");
    c.action();
    Base b=c;
    System.out.println("\n --- b.action()");
    System.out.println("\n --- b.s:"+b.s);
    System.out.println("\n --- c.s:"+c.s);
    }

执行结果如下:

下边我们逐过程来解释下其背后发生了什么,并解释我们所提出的问题。

类的加载过程

在 java 中,所谓的类加载指的是将类的相关信息加载到内存中。在 java 中类是动态加载的,当第一次使用这个类的时候才会加载,而且在加载一个类的时候会查看其父类是否被加载,如果没有则会加载其父类。

一个类的信息主要包含以下几个部分:

类的加载过程顺序如下:

  1. 分配内存保存类的信息

  2. 给类变量赋默认值

    注意: 数字类变量默认值都是 0,boolean 默认值是 false,char 是 \u0000,引用型变量默认值都是 null

  3. 加载父类

  4. 设置父子关系

  5. 执行类的初始化代码

以我们的例子来说,我们这里有三份类信息,分别是 ChildBaseObject,内存布局如下图所示:

对象的创建过程

在类加载之后,new Child()就会创建 Child 对象,创建 Child 对象过程包括:

  1. 分配内存
  2. 对所有实例变量赋默认值
  3. 执行实例初始化代码

在该部分分配的内存包括本类和所有父类的实例变量,不包含任何静态变量(因为类加载过程中这部分变量的内存已经分配完成)。实例的初始化代码执行先从父类开始,父类执行完成之后再执行子类。但需要注意的是在任何类执行初始化代码之前,任何实例变量都会被赋默认值。

但需要注意的是,每一个对象除了保存类的实例变量之外,还保存着实际类信息的引用。

方法调用的过程

在该部分我们分析前边所提出的问题,首先我们先来看 c.action(),这句代码的执行过程如下:

  1. 查看 c 的对象类型,找到 Child 类型,在 Child 类中找 action 方法,发现没有然后到父类中寻找。
  2. 在父类 Base 中找到了方法 action,开始执行 action 方法;
  3. action 先输出 start,然后发现需要调用 step()方法,就从 Child 类型中去寻找 step()方法;
  4. 在 Child 类型中找到了 Child 方法,执行 Child 类中的 step()方法,输出 Child 类中的两个变量 s(=10),a(=20)的值;
  5. 继续执行 action 方法输出 end

在此处我们可以发现,寻找要执行的实例方法的时候,是从对象的实际类型信息开始查找的,找不到的时候在查找父类的类型信息

接着我们来看 b.action()的执行过程,这句话实际上输出的结果和 c.aciton()方法输出的结果是一样的,这我们称之为 动态绑定,而 动态绑定 实现的机制就是根据对象的实际类型信息查找要执行的方法,子类型中查找不到才会查找父类。这里因为 b 和 c 执行的是相同的对象,所有执行的结果是一样的。

而对于变量部分我们发现,其访问过程是 静态绑定 的,即无论对于类变量还是实例变量访问的时候,访问的实际变量都和其对象类的类型绑定,如 b.s 和 c.s 分别访问的是 Base.s 和 Child.s。例子中实例变量都是 private 类型的,如果是 public 我们会发现 b.a 访问的是 Base 类定义的实例变量 a,而 c.a 访问的是对象中 Child 类定义的实例变量 a。

小结

在该部分我们分析的类和对象加载的过程,以及在过程中方法和代码块的执行顺序,我们可以得到以下结论。

方法和代码的执行顺序:

  1. 父类的类初始化代码块(static 代码块)
  2. 子类的类代码初始化块
  3. 父类的实例初始化代码块
  4. 父类的构造方法
  5. 子类的实例初始化代码块
  6. 子类的构造方法

重载方法的执行逻辑:

寻找要执行的实例方法的时候,是从对象的实际类型信息开始查找的,找不到的时候在查找父类的类型信息。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3190 引用 • 8214 回帖 • 1 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...