Java向上转型代码执行解析:方法重写与变量访问差异及内存分配
先把你的代码格式化后放在这里,方便对照理解:
public class demo { public static void main(String[] args) { aaa bb = new b(); System.out.println(bb.a); int c = bb.eat(); System.out.println(c); } } class aaa{ int a = 30; int eat() { int x = 60; System.out.println("CHEWING"); return x; } } class b extends aaa{ int a = 23; int eat() { //super.eat(); int x = 70; System.out.println("EaTING"); return x; } }
运行这段代码会得到这样的输出:
30 EaTING 70
也就是你遇到的「重写方法走子类逻辑,但变量取父类值」的现象,这本质是Java对成员方法和成员变量采用的绑定规则不同导致的,结合向上转型的特性,我给你一步步拆解:
一、向上转型的执行逻辑核心:静态绑定 vs 动态绑定
当你写aaa bb = new b();时,这就是Java里的向上转型——用父类类型的引用指向子类的实例对象。这时候,Java对方法和变量的处理逻辑完全不一样:
1. 成员方法:动态绑定(运行时决定)
Java的方法调用是运行时绑定的:编译阶段只会检查父类aaa有没有eat()方法(只要存在,编译就通过),但真正运行的时候,JVM会去看实际创建的对象类型(这里是b的实例),然后调用该类中重写的方法。
所以bb.eat()会执行子类b里的eat(),输出"EaTING"并返回70,这就是方法重写带来的多态特性。
2. 成员变量:静态绑定(编译时决定)
和方法完全不同,Java的成员变量是编译时绑定的。编译阶段,编译器只会看引用变量bb的声明类型(也就是aaa),直接把bb.a绑定到父类aaa的a变量上,完全不管实际对象是子类b的实例。
所以System.out.println(bb.a)会输出父类的30——这里子类的a和父类的a是两个完全独立的变量,只是名字相同而已,互相不影响。
二、对应的内存分配机制
要彻底理解这个行为,得看看JVM执行代码时的内存分配细节:
- 栈内存:存储局部变量
bb,它的类型是aaa,里面存的是堆内存中b实例的地址。 - 堆内存:当执行
new b()时,JVM会在堆里创建一个完整的b类实例,这个实例包含两部分:- 继承自父类
aaa的成员:也就是int a = 30(父类的实例变量); - 子类
b自己的成员:也就是int a = 23(子类的实例变量)。
这两个a在堆里是不同的内存位置,只是名字相同,互相独立。
- 继承自父类
- 方法区:存储
aaa和b的类信息,包括各自的eat()方法字节码。当调用方法时,JVM会根据对象的实际类型,去方法区找到对应的方法执行。
简单总结:堆里的b实例同时带着父类和子类的a,但因为bb是父类类型引用,编译时就定死了取父类的a;而方法调用是运行时看实际对象,所以走子类的eat()。
如果想访问子类的a,你可以把引用强制转回子类类型:((b)bb).a,这样就能拿到23了。
内容的提问来源于stack exchange,提问作者Sri Lalitha

