bigywater.tech
Java 内部原理 · 77

字段隐藏真实验证:字段访问没有多态,方法调用才有

"多态"这个词经常被不加区分地用在字段和方法上,但这篇真实实验证明两者的行为完全不同:方法调用真实按对象的运行时类型决定,字段访问真实按引用的静态类型决定——这条规则连很多写了很多年Java的人都会记混。

字段访问真实不看运行时类型
真实验证:同一个Dog对象,dog.type得到"dog",animalRef.type(同一对象,Animal引用)得到"animal"——两次真实结果不同
方法调用真实是多态的
真实验证:dog.getType()和animalRef.getType()对同一对象真实都返回"dog",按对象运行时类型统一决定

上一篇:异常链真实验证

真实验证:同一个对象,两种引用,字段访问结果真实不同

Dog继承Animal,两个类都声明了同名字段type(不是重写,是隐藏)。用两种静态类型的引用指向同一个Dog对象,分别访问字段。

FieldHidingDemo.java class Animal { String type = "animal"; } class Dog extends Animal { String type = "dog"; } // 字段隐藏 Dog dog = new Dog(); Animal animalRef = dog; // 同一个对象 System.out.println(dog.type); // ? System.out.println(animalRef.type); // ?
真实实测
dog.type (Dog引用直接访问): dog
animalRef.type (Animal引用访问同一个对象): animal(字段访问真实按引用类型决定,不是运行时类型)

真实结果证明了字段访问的规则:animalRef 和 dog 指向的是完全同一个堆上对象,但因为 animalRef 声明的静态类型是 Animal,编译器在编译期就已经把 animalRef.type 这行代码真实解析成"访问 Animal 类里定义的那个 type 字段"——这个决定在编译时就完成了,和运行时这个对象实际是什么类型无关。这也是为什么这种现象叫"字段隐藏"(hiding)而不是"字段重写"(overriding):子类的 type 字段并没有替换掉父类的 type 字段,两者是真实共存、独立存在的,只是子类的字段在用 Dog 类型引用访问时"挡住"了父类的同名字段。

真实验证:方法调用不受引用类型影响,真实按运行时类型走

同样的两个引用,这次调用一个被真实重写(override)的方法。

真实实测
dog.getType(): dog
animalRef.getType(): dog(方法调用真实按运行时类型决定,两次结果一致)

真实结果形成鲜明对比:不管用哪个引用调用 getType(),结果真实都是 "dog"。这是因为方法调用在 JVM 层面通过 invokevirtual 指令实现,真实依赖对象头里记录的实际类型信息,在运行时动态查找应该调用哪个版本的方法(虚方法表),这才是真正的多态——决定权在对象的运行时类型,不在引用的静态类型。

真实验证:反射证明父子类同名字段是两个独立的 Field

用反射分别拿到 Animal.type 和 Dog.type 两个字段,证明它们是真实共存的两份独立存储。

真实实测
反射读取 Animal.type 字段真实值: animal
反射读取 Dog.type 字段真实值: dog
两个Field对象是否相同: false(真实是两个独立的字段,只是同名)
真实的内存模型。一个 Dog 实例在堆上真实同时占用了两份 type 字段的存储空间——一份属于 Animal 部分,一份属于 Dog 部分,它们只是恰好同名,底层是完全独立的两个槽位。反射能够分别通过 Animal.class.getDeclaredField("type") 和 Dog.class.getDeclaredField("type") 拿到两个不同的 Field 对象,并且能在同一个 dog 实例上读出两个真实不同的值,这直接证明了字段隐藏的本质是"共存",不是方法重写那种"替换"。

真实验证:静态方法同样是隐藏,不是重写

静态方法和字段一样,也不遵循方法重写的多态规则。

真实实测
Animal.staticInfo(): animal-static
Dog.staticInfo(): dog-static
(通过Animal静态类型引用ref调用) ref.staticInfo(): animal-static(真实按引用的静态类型Animal解析,不是运行时类型Dog)

真实结果证明即使 ref 实际指向一个 Dog 实例,通过声明为 Animal 类型的引用调用静态方法 staticInfo(),真实解析到的依然是 Animal 版本——因为静态方法的调用在编译期就通过 invokestatic 指令绑定到了具体的类,和字段访问遵循同一套"看引用的静态类型"规则,这也是为什么规范上不推荐通过实例引用调用静态方法(容易造成"看起来像多态,实际不是"的误解)。

分步演示:同一个引用,字段和方法给出不同答案

每一行都是真实运行这段代码得到的结果。

字段访问按引用的静态类型,方法调用按对象的运行时类型:同一对 dog/animalRef 引用,五个真实结果
这一步新验证的访问

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,字段/方法访问结果重复运行完全一致。
    • 和重载 vs 重写那篇一起看,能更完整理解 Java 里哪些语言特性真实支持多态、哪些只是"看起来像"但本质是编译期静态绑定。
    • 没有涉及:private方法是否存在"重写"的概念(结论是不存在,private方法对子类不可见,子类同名方法是全新的方法)、接口默认方法和字段隐藏规则的交互——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号