前言
本文主要针对一些比较模糊的问题进行总结:
- 引用是父类,实例是子类
- super和this
父类和子类的引用情况
类型转换
父类的引用可以指向子类的对象(向上转型),子类引用不能指向父类对象。这个很好理解,子类实例化会执行父类的构造器,此时父类的属性已经在内存中了,使用父类的引用可以访问到父类的属性信息;但是父类实例化不会执行子类的构造器,因此内存中不会有子类的属性信息,使用子类的引用无法访问到子类的成员信息。这里有个口诀对于对象的类型:编译看左边,运行看右边,静态全看左。
1 | Father f = new Son(); // ok |
这里比较特殊的是,父类引用指向子类的对象,该引用只能调用父类定义的成员变量和成员方法(因为编译看左边),而在运行中,调用同名的成员变量会调用父类的成员变量,如果调用同名的成员方法(即重写的方法),则会调用子类的方法。这里很奇怪,既然运行看右边,为什么我访问的不是子类对象中同名的成员变量呢?
这和Java的运行机制有关,Java中的成员变量、静态成员方法、private以及final方法都是静态绑定的,所谓静态绑定是指在编译期绑定,这时候引用f
已经和父类的成员变量绑定了;而普通的成员方法是动态绑定的,也就是运行时绑定,此时引用f
在调用方法的时候是访问的是子类方法表的地址。也就是说,Java的多态是指在运行时多态,只是指普通成员方法的多态。
1 | public class Father { |
这里补充一下:虽然引用f
无法直接调用子类的成员变量和父类没有的成员方法,但是在运行时,这些属性仍然是开辟了空间的,但是无法访问(主要是因为编译不过)。这里给出一个验证,我们知道反射是典型的在运行时可以操作对象的方式,这里通过反射在运行时强行去找f
的成员变量,可以看到实际上f
在运行时是一个子类的实例对象,通过反射可以访问到父类没有的变量或者方法(代码只写了变量),但是因为静态绑定了,所以直接通过f.name
显示的是父亲的name
。
1 | public static void main(String[] args) { |
Super和This
大家都知道两点:
- 子类初始化会调用父类的构造器
- 子类中使用super可以调用父类的变量和方法(除了
private
修饰的)
但是这里要强调一下,子类实例化不会创建父类的实例,调用父类的构造器只是在内存中开辟了父类属性的空间,也就是说和this
不同,super
并不指代一个对象。如果调用super.hashcode()
就会发现结果和this.hashcode()
的值一样,因为根本就没有父类对象,调用父类的hashcode
和子类的hashcode
得到的都是子类的hashcode
,从这可以推导得出,就算是在父类的构造器中调用this.hashcode()
得到的也是子类的hashcode
。
总结一下就是:super
只是一个标志,不指向任何对象,它能调用父类的构造器和方法,并不是说指向了父类对象,因为根本没有父类对象被创建。
另外,hashcode
方法是运行时调用的,因此上述的结论和引用是不是子类类型并无关系。
1 | // 重写Son和Father |
总结
Java继承主要就是要处理两个问题:一、什么东西是静态绑定的;二、子类实例化,会不会实例化一个父类对象?懂了这两个问题,所有问题都能推导出来。