在 Java 的继承体系中,有一个常见但容易令人困惑的细节:为何直接访问字段时,它似乎对多态“免疫”?无论对象实际类型如何,引用的声明类型总能一锤定音。这背后,其实是语言设计上的一种有意为之。

字段访问:编译期就已“盖棺定论”
问题的根源在于,字段访问采用 静态绑定(编译期绑定),而非像方法调用那样的动态绑定。Java 编译器在编译阶段,就根据引用的声明类型确定了要访问的字段地址。即便你指向的是一个子类对象,只要引用类型是父类,访问的就是父类中定义的字段。
- 举个例子:
Super sup = new Sub();,那么sup.field访问的必然是Super.field,哪怕Sub类里也定义了一个同名字段。 - 这种同名关系,在子类中不是“覆盖”,而是“遮蔽”(shadowing)或“隐藏”(hiding)。父类和子类的字段在内存中是两个完全独立的变量,各占各的空间,互不干扰。
- 字段没有虚方法表(vtable)参与调度,JVM 不为其做运行时类型分派,因此也就谈不上“多态”。
没有动态分派的“调度表”
多态之所以成立,核心在于动态绑定:JVM 在运行时查虚方法表,根据对象的实际类型决定调用哪个方法。但字段没有这套机制。
- 方法重写(override)会替换父类在 vtable 中的条目;而字段声明,只是新增一个独立变量。
- 子类字段与父类字段,各自占用不同的内存偏移量。JVM 按引用类型直接计算字段位置,不查表,不跳转,简单直接。
- 即便子类字段和父类字段类型相同、名字相同,它们在内存里依然是两个完全独立的存储单元,谁也不影响谁。
设计上的刻意区分:状态与行为
Java 语言规范有意将字段访问和方法调用划分为两类不同的语义:字段代表状态,方法代表行为。状态归属由声明类型决定,行为实现由实际类型决定。
- 这个设计保证了字段访问的可预测性和编译期安全性。程序员读代码时,能一眼看出这个变量到底是谁的。
- 如果字段也支持多态,那字段的含义就会随着运行时类型“漂移”,这会严重破坏封装性,让调试变得难以捉摸。
- 因此,当需要“多态式字段语义”时,正确的做法是通过 getter/setter 方法,因为方法天然支持动态绑定。
避免混淆的最佳实践:用方法代替直接字段访问
当需要基于实际类型返回不同值时,别直接暴露字段,而是通过方法封装。
- 把
public int value;改成private int value;+public int getValue() { return value; }。 - 子类重写
getValue()方法,就能获得真正的多态效果。 - 这样做既保持了字段的静态确定性,又让行为具备了动态性,完全符合面向对象的设计原则。
