MST
星途 面试题库

面试题:Java抽象类与具体类在内存分配和初始化过程中的差异

深入分析Java抽象类和具体类在内存分配机制以及初始化步骤上的不同点,从类加载、对象创建等方面展开说明,并结合实际代码示例解释可能出现的问题。
11.8万 热度难度
编程语言Java

知识考点

AI 面试

面试题答案

一键面试

内存分配机制

  1. 类加载阶段
    • 抽象类:当Java虚拟机加载抽象类时,会为其分配方法区的内存空间,存储类的元数据信息,如类名、父类、接口、字段和方法等。但抽象类不能被实例化,所以在类加载时不会为其对象分配堆内存。例如:
abstract class AbstractClass {
    protected int num;
    public abstract void abstractMethod();
}

在加载 AbstractClass 时,方法区中会有它的相关信息,但不会在堆上有对应对象。

  • 具体类:具体类在类加载时同样在方法区分配内存存储元数据。与抽象类不同的是,具体类可以被实例化,当实例化时会在堆内存中为其对象分配空间。例如:
class ConcreteClass extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("Concrete implementation");
    }
}
  1. 对象创建阶段
    • 抽象类:由于抽象类不能直接实例化,不存在像具体类那样在堆上创建对象的过程。如果尝试 AbstractClass obj = new AbstractClass(); 会在编译时报错。
    • 具体类:具体类在创建对象时,首先在堆上分配足够的内存空间来存储对象的实例变量。然后根据对象的类型,将对象的引用指向堆内存中的对象。例如:
ConcreteClass obj = new ConcreteClass();

这里 new ConcreteClass() 在堆上创建对象,obj 引用指向该对象。

初始化步骤

  1. 抽象类
    • 类加载时:静态成员变量和静态代码块会按照它们在类中出现的顺序进行初始化。例如:
abstract class AbstractClass {
    static int staticVar = 10;
    static {
        System.out.println("Abstract class static block");
    }
}

AbstractClass 被加载时,staticVar 被初始化为10,静态代码块被执行。抽象类的普通成员变量和构造函数不会在类加载时初始化,因为抽象类不能直接实例化。

  • 子类实例化时:当子类(具体类)继承抽象类并实例化时,会先调用抽象类的构造函数(如果有)来初始化抽象类部分的成员变量。例如:
abstract class AbstractClass {
    protected int num;
    public AbstractClass() {
        num = 5;
    }
}
class ConcreteClass extends AbstractClass {
    public ConcreteClass() {
        System.out.println("Concrete class constructor, num = " + num);
    }
}

new ConcreteClass() 时,先调用 AbstractClass 的构造函数,num 被初始化为5,然后执行 ConcreteClass 的构造函数,输出 Concrete class constructor, num = 5。 2. 具体类

  • 类加载时:和抽象类一样,静态成员变量和静态代码块按顺序初始化。例如:
class ConcreteClass {
    static int staticVar = 20;
    static {
        System.out.println("Concrete class static block");
    }
}

ConcreteClass 被加载时,staticVar 被初始化为20,静态代码块被执行。

  • 对象创建时:先为对象的实例变量分配内存空间并赋予默认值(如 int 为0,Objectnull 等),然后按照声明顺序初始化实例变量,最后执行构造函数。例如:
class ConcreteClass {
    int num = 1;
    public ConcreteClass() {
        num = 2;
        System.out.println("Concrete class constructor, num = " + num);
    }
}

new ConcreteClass() 时,num 先被赋予默认值0,然后初始化为1,最后在构造函数中被赋值为2并输出。

可能出现的问题

  1. 抽象类使用不当:如果在抽象类中定义了抽象方法,但子类没有完全实现这些抽象方法,子类也必须声明为抽象类,否则会编译错误。例如:
abstract class AbstractClass {
    public abstract void abstractMethod();
}
class SubClass extends AbstractClass {
    // 没有实现abstractMethod方法
}
// 这里SubClass没有实现抽象方法,应声明为abstract,否则编译报错
  1. 初始化顺序问题:在继承体系中,如果没有正确理解抽象类和具体类的初始化顺序,可能导致在使用成员变量时得到未预期的值。例如,在抽象类构造函数中访问子类重写的方法,由于子类还未完全初始化,可能得到错误的结果。
abstract class AbstractClass {
    public AbstractClass() {
        callSubMethod();
    }
    public void callSubMethod() {
        System.out.println("Abstract class method");
    }
}
class ConcreteClass extends AbstractClass {
    private int num = 10;
    @Override
    public void callSubMethod() {
        System.out.println("Concrete class method, num = " + num);
    }
}
// 当new ConcreteClass()时,在AbstractClass构造函数中调用callSubMethod,此时num还未初始化,会输出"Concrete class method, num = 0"

这里在 AbstractClass 的构造函数中调用 callSubMethod,实际调用的是 ConcreteClass 重写的方法,但此时 ConcreteClassnum 还未初始化,得到的值是默认值0,而不是预期的10。