"多态"这个词经常被不加区分地用在字段和方法上,但这篇真实实验证明两者的行为完全不同:方法调用真实按对象的运行时类型决定,字段访问真实按引用的静态类型决定——这条规则连很多写了很多年Java的人都会记混。
上一篇:异常链真实验证
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 指令实现,真实依赖对象头里记录的实际类型信息,在运行时动态查找应该调用哪个版本的方法(虚方法表),这才是真正的多态——决定权在对象的运行时类型,不在引用的静态类型。
用反射分别拿到 Animal.type 和 Dog.type 两个字段,证明它们是真实共存的两份独立存储。
反射读取 Animal.type 字段真实值: animal 反射读取 Dog.type 字段真实值: dog 两个Field对象是否相同: false(真实是两个独立的字段,只是同名)
静态方法和字段一样,也不遵循方法重写的多态规则。
Animal.staticInfo(): animal-static Dog.staticInfo(): dog-static (通过Animal静态类型引用ref调用) ref.staticInfo(): animal-static(真实按引用的静态类型Animal解析,不是运行时类型Dog)
真实结果证明即使 ref 实际指向一个 Dog 实例,通过声明为 Animal 类型的引用调用静态方法 staticInfo(),真实解析到的依然是 Animal 版本——因为静态方法的调用在编译期就通过 invokestatic 指令绑定到了具体的类,和字段访问遵循同一套"看引用的静态类型"规则,这也是为什么规范上不推荐通过实例引用调用静态方法(容易造成"看起来像多态,实际不是"的误解)。
每一行都是真实运行这段代码得到的结果。