Java 中父子类同名字段的隐藏机制与对象内存布局解析

java 子类声明与父类同名字段时,并非覆盖,而是**字段隐藏(field hiding)**:子类对象在内存中实际包含两份独立的 `x` 字段(`parent.x` 和 `child.x`),分别由各自构造器初始化,通过 `this.x` 和 `super.x` 可明确访问不同副本。

在 Java 中,当子类 C 继承父类 P 并定义了同名字段(如 int x;),这并非“重写”或“覆盖”,而是一种明确的语言特性——字段隐藏(Field Hiding),由《Java 语言规范》(JLS §8.3)明确定义:子类中同名字段会隐藏所有可访问的父类同名字段。关键在于:隐藏 ≠ 合并,它导致同一个对象实例中

物理存在两个独立的 x 字段,分别隶属于父类和子类的继承层级。

内存结构:一个对象,两份字段

虽然 new C() 只创建一个对象实例,但该实例的内存布局包含:

  • 父类部分(P 的字段):含 P.x,初始值为 1(由 P() 构造器设置);
  • 子类扩展部分(C 的字段):含 C.x,初始值为 2(由 C() 构造器设置)。

二者在内存中是完全独立的变量,占据不同偏移地址,互不干扰。

访问规则决定输出差异

你观察到的输出:

c.x: 2
c.getX(): 1

正是字段隐藏行为的直接体现:

  • c.x → 编译期根据变量静态类型 C 解析,访问的是 C.x(值为 2);
  • c.getX() → getX() 在 P 中定义,其方法体中的 x 指代的是 P 类作用域内的 x(即 P.x),因此返回 1。

✅ 正确访问双字段的示例:

class Child extends Parent {
    int x;

    Child() {
        super(); // 显式调用父构造器(可省略,但强调执行顺序)
        this.x = 2;   // 初始化 Child.x
    }

    void printBothXs() {
        System.out.println("this.x (Child.x): " + this.x);     // 输出 2
        System.out.println("super.x (Parent.x): " + super.x); // 输出 1
    }
}

重要注意事项

  • 无法从外部类直接访问 super.x:c.super.x 是非法语法。super 关键字仅在子类非静态方法/构造器内部有效;
  • ⚠️ 避免字段隐藏:这是易出错的设计陷阱。推荐使用不同字段名,或通过 protected 字段 + getter/setter 统一管理;
  • ? 构造器执行顺序:new C() 时,JVM 严格按 P() → C() 顺序执行,确保父类字段先初始化,但不会覆盖子类同名字段;
  • ? 与方法重写的本质区别:方法可被重写(动态绑定),字段只能被隐藏(静态绑定)。这是理解本问题的核心分水岭。

总结来说,Java 的字段隐藏机制让单个对象能安全容纳多份同名字段,提升继承灵活性,但也要求开发者清晰区分 this. 与 super. 的语义边界——这不是 bug,而是 JLS 精心设计的、可预测的语言特性。