抽象类及其抽象子类 与 普通类及其普通子类
好的,我将用中文详细对比 抽象类及其抽象子类 与 普通类及其普通子类 在内存分配和使用上的差异,基于 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()的实现字节码,继承name和age字段,继承eat()。Car:存储speed字段信息,覆盖move()的字节码,继承model。- 堆内存:
Animal和Mammal:无对象实例,无堆内存分配。Vehicle:对象包含model字段。Dog:对象包含name(从Animal)、age(从Mammal)字段。Car:对象包含model(从Vehicle)、speed字段。- 方法表:
Animal:makeSound()(占位)、eat()(字节码地址)。Mammal:继承makeSound()(占位)、eat()。Vehicle:move()(字节码地址)。Dog:makeSound()(实现地址)、eat()(继承)。Car:move()(覆盖的实现地址)。
4. 总结¶
| 特性 | 抽象类 | 抽象子类 | 普通类 | 普通子类 |
|---|---|---|---|---|
| 类加载(方法区) | 存储抽象方法签名和非抽象方法实现,标记为不可实例化 | 继承父类,抽象方法可不实现,标记为不可实例化 | 存储完整方法实现,可实例化 | 继承父类,提供抽象方法实现(若有),可实例化 |
| 实例化(堆) | 不可实例化,无堆内存 | 不可实例化,无堆内存 | 可实例化,分配字段内存 | 可实例化,分配自身和父类字段内存 |
| 方法表 | 抽象方法占位,非抽象方法有实现 | 继承父类方法表,抽象方法可占位 | 所有方法有实现 | 继承并覆盖父类方法,抽象方法需实现 |
| 内存布局 | 无对象,仅方法区元信息 | 无对象,继承父类元信息 | 对象包含字段和对象头 | 对象包含自身和父类字段 |
- 抽象类和抽象子类:主要在方法区存储模板信息,抽象方法仅占位,节省方法体空间,但不可实例化,无堆内存占用。
- 普通类和普通子类:方法区存储完整实现,堆中分配对象内存,内存占用较多,因包含所有字段和方法实现。
- 关键差异:抽象类及其子类的抽象性导致其不占用堆内存,方法表可能包含未实现方法;普通类及其子类提供完整实现,支持实例化和直接调用。