Skip to content

抽象类及其抽象子类 与 普通类及其普通子类

好的,我将用中文详细对比 抽象类及其抽象子类普通类及其普通子类 在内存分配和使用上的差异,基于 Java 的内存模型和运行时机制。以下分析将从类加载、实例化、方法表和内存布局等角度进行对比。


1. 基本概念

  • 抽象类:使用 abstract 关键字修饰,不能直接实例化,可能包含抽象方法(无方法体)和非抽象方法(有方法体)。其设计目的是作为模板,被子类继承并实现。
  • 抽象子类:继承抽象类的子类,且本身也被声明为 abstract,可以选择实现或不实现父类的抽象方法,仍然不能实例化。
  • 普通类:不使用 abstract 修饰,可以直接实例化,包含完整的字段、方法和构造方法。
  • 普通子类:继承普通类或抽象类的非抽象类,必须实现父类(若为抽象类)的所有抽象方法,可以直接实例化。

2. 内存对比

以下从 类加载实例化方法表内存布局 四个方面进行对比。

2.1 类加载(方法区)

  • 抽象类
  • 在 JVM 的方法区(Method Area)中存储字节码,包含类结构信息(常量池、字段、方法表等)。
  • 抽象方法仅存储方法签名(方法名、参数、返回值类型),没有方法体字节码。
  • 非抽象方法存储完整的字节码实现。
  • 标记为 abstract,JVM 禁止直接实例化。
  • 抽象子类
  • 同样在方法区存储字节码,继承父类的字段和方法。
  • 如果不实现父类的抽象方法,这些方法在子类的类结构中仍然只有签名,无方法体。
  • 如果实现了部分抽象方法,则存储对应方法的字节码。
  • 标记为 abstract,不能实例化。
  • 普通类
  • 在方法区存储完整的字节码,包括所有方法的实现(无抽象方法)。
  • 可以直接实例化,类结构中包含完整的字段、方法和构造方法信息。
  • 普通子类
  • 在方法区存储字节码,继承父类的字段和方法。
  • 如果父类是抽象类,必须为所有抽象方法提供实现,字节码包含这些方法的完整实现。
  • 如果父类是普通类,直接继承其方法和字段,无需额外实现。
  • 可以直接实例化。

对比: - 抽象类和抽象子类的字节码可能包含未实现的抽象方法(仅方法签名),占用方法区空间较少(因缺少方法体)。 - 普通类和普通子类的字节码包含所有方法的完整实现,方法区空间占用略多。 - 抽象类和抽象子类在类加载时被标记为不可实例化,JVM 会对此进行约束。

2.2 实例化(堆内存)

  • 抽象类
  • 不能实例化,因此不会在堆(Heap)中分配对象内存。
  • 仅作为模板存在,字段和非抽象方法的字节码在方法区中,供子类使用。
  • 抽象子类
  • 同样不能实例化,不会分配堆内存。
  • 如果有自己的字段或非抽象方法,这些信息在方法区中存储,等待非抽象子类实例化时使用。
  • 普通类
  • 可以实例化,JVM 在堆中为对象分配内存,包含类的所有字段(包括私有字段)。
  • 构造方法执行时,初始化字段值,分配的对象内存包含类定义的实例变量。
  • 普通子类
  • 可以实例化,堆中分配的对象内存包含子类自身字段和从父类(普通类或抽象类)继承的字段。
  • 构造方法调用时,先调用父类构造方法(通过 super()),确保父类字段初始化,然后初始化子类字段。

对比: - 抽象类和抽象子类不占用堆内存,因为它们不能创建对象。 - 普通类和普通子类的对象在堆中分配内存,内存大小取决于类及其父类的字段总数。 - 普通子类的堆内存可能包含抽象父类的字段(如果继承自抽象类),但抽象类/子类本身不直接占用堆空间。

2.3 方法表(动态分派)

  • 抽象类
  • 在方法区中维护方法表(Method Table,类似虚函数表),包含所有方法的引用。
  • 抽象方法在方法表中仅占位(指向空实现或标记为抽象),等待子类提供具体实现。
  • 非抽象方法指向实际的字节码地址。
  • 抽象子类
  • 继承父类的方法表,覆盖或补充父类的方法。
  • 未实现的抽象方法在方法表中仍为占位状态,已实现的方法指向具体的字节码。
  • 普通类
  • 方法表包含所有方法的实际字节码地址,无抽象方法。
  • 方法调用通过方法表直接解析到具体实现。
  • 普通子类
  • 方法表继承自父类,覆盖或补充父类方法。
  • 如果父类是抽象类,普通子类必须为抽象方法提供实现,方法表中指向这些实现的字节码地址。
  • 如果父类是普通类,方法表直接继承或覆盖父类方法的实现。

对比: - 抽象类和抽象子类的方法表可能包含“空”引用(抽象方法),需要子类填充实现。 - 普通类和普通子类的方法表始终指向完整的字节码实现,支持直接调用。 - 动态分派时,抽象类的子类需要额外的继承链解析,而普通类的子类调用更直接。

2.4 内存布局(对象结构)

  • 抽象类
  • 无对象实例,因此无堆内存布局。
  • 字段信息存储在方法区,供子类继承。
  • 抽象子类
  • 同样无对象实例,字段信息在方法区,继承父类字段并可能添加新字段。
  • 普通类
  • 对象在堆中的内存布局包括:对象头(包含类指针、标记字段等)、实例字段(包括所有非静态字段)、对齐填充。
  • 字段按声明顺序排列,可能包含父类的字段(如果有继承)。
  • 普通子类
  • 对象内存布局包含子类字段和继承的父类字段(无论父类是普通类还是抽象类)。
  • 如果父类是抽象类,子类的对象内存布局仍包含父类的字段,但方法实现由子类提供。

对比: - 抽象类和抽象子类仅在方法区存储元信息,无堆内存布局。 - 普通类和普通子类的对象在堆中分配内存,包含完整的字段布局,内存占用取决于字段数量和类型。 - 普通子类的内存布局可能因继承抽象类的字段而稍复杂,但整体结构一致。


3. 示例代码与内存分析

以下通过代码示例说明内存差异:

// 抽象类
abstract class Animal {
    String name; // 字段
    abstract void makeSound(); // 抽象方法
    void eat() { System.out.println("Eating"); } // 非抽象方法
}

// 抽象子类
abstract class Mammal extends Animal {
    int age; // 新增字段
    // 未实现 makeSound()
}

// 普通类
class Vehicle {
    String model;
    void move() { System.out.println("Moving"); }
}

// 普通子类(继承抽象类)
class Dog extends Mammal {
    @Override
    void makeSound() { System.out.println("Woof!"); }
}

// 普通子类(继承普通类)
class Car extends Vehicle {
    int speed;
    @Override
    void move() { System.out.println("Car moving at " + speed); }
}

内存分析:

  • 方法区
  • Animal:存储 name 字段信息、makeSound() 方法签名(无实现)、eat() 方法字节码。
  • Mammal:继承 Animal,存储 age 字段信息,makeSound() 仍为抽象方法签名,继承 eat()
  • Vehicle:存储 model 字段信息、move() 方法字节码。
  • Dog:存储 makeSound() 的实现字节码,继承 nameage 字段,继承 eat()
  • Car:存储 speed 字段信息,覆盖 move() 的字节码,继承 model
  • 堆内存
  • AnimalMammal:无对象实例,无堆内存分配。
  • Vehicle:对象包含 model 字段。
  • Dog:对象包含 name(从 Animal)、age(从 Mammal)字段。
  • Car:对象包含 model(从 Vehicle)、speed 字段。
  • 方法表
  • AnimalmakeSound()(占位)、eat()(字节码地址)。
  • Mammal:继承 makeSound()(占位)、eat()
  • Vehiclemove()(字节码地址)。
  • DogmakeSound()(实现地址)、eat()(继承)。
  • Carmove()(覆盖的实现地址)。

4. 总结

特性 抽象类 抽象子类 普通类 普通子类
类加载(方法区) 存储抽象方法签名和非抽象方法实现,标记为不可实例化 继承父类,抽象方法可不实现,标记为不可实例化 存储完整方法实现,可实例化 继承父类,提供抽象方法实现(若有),可实例化
实例化(堆) 不可实例化,无堆内存 不可实例化,无堆内存 可实例化,分配字段内存 可实例化,分配自身和父类字段内存
方法表 抽象方法占位,非抽象方法有实现 继承父类方法表,抽象方法可占位 所有方法有实现 继承并覆盖父类方法,抽象方法需实现
内存布局 无对象,仅方法区元信息 无对象,继承父类元信息 对象包含字段和对象头 对象包含自身和父类字段
  • 抽象类和抽象子类:主要在方法区存储模板信息,抽象方法仅占位,节省方法体空间,但不可实例化,无堆内存占用。
  • 普通类和普通子类:方法区存储完整实现,堆中分配对象内存,内存占用较多,因包含所有字段和方法实现。
  • 关键差异:抽象类及其子类的抽象性导致其不占用堆内存,方法表可能包含未实现方法;普通类及其子类提供完整实现,支持实例化和直接调用。

如果你需要更深入的内存分析或具体代码示例,请告诉我!