案例
class Father {
int num = 111;
public void fun() {
System.out.println("father");
}
}
class Son extends Father {
int num = 222;
public void fun() {
System.out.println("son");
}
}
这边在Son类重新定义了成员变量的方法。
子类对象内存分配过程
- 首先是类加载,需要父类先加载,然后子类加载。
- 分配内存
- 赋0值
- 写对象头
- 走init方法
分配内存的问题,到底有几个对象?
调用子类构造器的时候,会先去调用父类的构造器,难道这就意味着,创建子类对象的时候,同时也创建了一份父类对象?也就是说,在堆区有两个对象?
不是的,子类对象会拥有父类定义的所有成员变量,但是,他不一定有父类成员变量的访问权限。可以想象为,子类对象里面套了一个父类的对象,但是本质上堆区只有一个子类对象。
怎么证明呢?我们在Son中输出一下super的类型不就知道了?
class Son extends Father {
public void test () {
System.out.println(super.getClass().getName); // 结果为Son
}
}
关于权限问题,文章最上面的两个类中都有num成员变量,Son其实拥有两份num,分别可以通过(this.)num和super.num进行访问。因为父类权限没有设置成private,因此子类可以访问。
如果父类中设置为private int num,那么子类调用super.num将会出错。
有人会说,既然父类的private属性子类访问不了,那么还分配给子类对象,不是浪费空间?这种想法是错的,因为我们仍然可以通过父类的public方法对private域进行间接访问。
刚才讲了继承成员变量的问题,那么方法、静态变量呢?
注意,堆区存储的仅仅是成员变量,而方法,静态变量这些都应该是在方法区的(虽然JDK7以后静态变量在堆区,不过是在Class对象内部),和成员变量组成的堆对象不是存储在一起的。堆对象只是一个存储成员变量的一团东西。
所以继承方法,这个严格上来说,其实是动态链接的功劳,在实际运行过程中,某些编译期无法确定的方法调用会进行一个动态链接的动作,最终链接到的方法可能是父类中的方法,也可能是子类中的方法。
Father son = new Son();
son.fun();
上述代码优先是调用运行时类型Son的对应方法,找不到才会去父类中找。
所以,当前运行时类型Son如果能找到这个方法,调用效果就等同于重写;如果找不到,调了父类的方法,效果等同于继承。
不要以为调用到了父类方法,他就是在一个父类的对象里面运行的,上面说过了,只会有一个子类对象。方法区的方法可以想象为共享的东西,我们拿这个东西放到子类对象里来使用了一次。这些方法其实不会和调用方类型有任何强绑定。
如果在Father中定义
public void fun() {
System.out.println(getClass().getName);
}
然后在子类Son中我们不重写这个方法,按照动态链接,调用到的是父类的方法
son.fun(); // 输出的仍然是Son,也就是虽然调用了父类方法,但是只是拿父类方法在我们子类对象中使用了一下,所在类型还是Son
另外,讲一下编译器解决某些方法调用的问题,编译器的逻辑就是,如果你的编译类型从父类继承来了这个方法、或者你自己声明了这个方法,那么你调用没事;但是如果自己没声明,父类又没声明,那么编译不通过。
举例来说
class Father {
}
class Son extends Father {
public void fun() {
System.out.println("son");
}
}
// 使用
Father son = new Son();
son.fun(); // 编译不通过
上面的对象son的编译类型是Father,但是Father自己没声明fun,Father的父类也没有fun,那么调用fun不合法,编译不通过。
静态变量和上面说的方法重写,有类似的效果,也有类似于动态链接的效果,子类中如果重新声明父类中的静态变量,假如是var,那么son.var就是调用子类中的静态变量,如果子类没有重新声明,那么就是调用父类中的var。
但是如果使用Father.var来调用,这就没有动态链接效果了,因为他是一个明确的调用,也就是Father类的类变量,肯定就是Father中的var。