深入理解重写、隐藏、和静、动态绑定的本质
问题引入
大家都知道成员方法能被继承和重写,那么静态变量或者静态方法能否被子类继承?能否被重写?
于是写了一段代码先验证下:
//定义父类
public class SuperClass {
static int SuperVar = 10;
public static void method(){
System.out.println("this SuperClass");
}
}
//定义子类
public class SubClass extends SuperClass{
}
//验证是否继承
public class Demo {
public static void main(String[] args) {
SubClass sub = new SubClass();
sub.method();
System.out.println(sub.SuperVar);
}
}
输出结果:
this SuperClass
10
明显看出,父类的静态方法和成员是可以被继承的,那么可以被重写吗?修改代码继续验证:
//定义父类
public class SuperClass {
static int SuperVar = 10;
public static void method(){
System.out.println("this SuperClass");
}
}
//定义子类,重新定义method方法以及SuperVar
public class SubClass extends SuperClass{
static int SuperVar = 20;
public static void method(){
System.out.println("this is SubClass");
}
}
//验证是否被重写
public class Demo {
public static void main(String[] args) {
SubClass sub = new SubClass();
sub.method();
System.out.println(sub.SuperVar);
}
}
输出结果:
this is SubClass
20
看到这我以为这个问题就结束了,然后顺手百度了一下再次验证,得到的答案是静态方法无法被重写,只能被隐藏,然后发现了一个新名词叫**隐藏**,通过一个例子来验证下吧。
//定义父类
public class SuperClass {
static int SuperVar = 10;
public static void method(){
System.out.println("this SuperClass");
}
}
//定义子类,重新定义method方法以及SuperVar
public class SubClass extends SuperClass{
static int SuperVar = 20;
public static void method(){
System.out.println("this is SubClass");
}
}
//验证是否被重写
public class Demo {
public static void main(String[] args) {
SuperClass sup = new SubClass();//向上转型
sup.method();
System.out.println(sup.SuperVar);
}
}
这里用父类引用指向子类实例,向上转型,熟悉多态(不熟悉请移步实例通俗理解多态)的朋友应该能立马想到这不就是多态吗,这个输出肯定和上面输出一样,肯定调用子类方法呀,但是现实却是:
this SuperClass
10
**指向子类的父类引用依然调用父类静态方法,**其实多态那篇博客中已经写过调用的规则,但未作出解释,接下来可以一探本质了。
再次展示向上转型后的调用规则:
类型 | 调用方式 |
---|---|
同名静态方法 | 调用父类的 |
同名成员方法 | 调用子类的 |
同名静态变量 | 调用父类的 |
同名成员变量 | 调用父类的 |
对于这一现象(指向子类的父类引用依然调用父类静态方法),引用Java中用static修饰的方法能否被子类重写?给出的解释:
当子类中出现了与父类static关键字修饰的方法同名同类型同参数列表不降低访问权限的方法(就是打算进行重写的那一套规则),这并不是代表子类重写了父类的static关键字修饰的方法,而是:
子类的该方法只是将父类的方法进行了隐藏,而非重写。这两个方法没有关系!
父类引用指向子类对象时,只会调用父类的静态方法。所以,它们的行为也不具有多态性!
这个答案像是对以上现象的一种总结:静态方法不会重写,而是被隐藏,父类引用指向子类对象调用父子类同名静态方法时,只会调用父类,不具有多态性。
看完后什么是隐藏什么是重写我还是不知道?那先看一下隐藏和重写的定义吧:
重写
当子类继承父类,子类中方法的名称,参数类型,参数个数与父类中的方法都完全一致,则认为子类中的方法重写了父类中的方法。
重写需要注意几点:无法重写被final等修饰的方法;子类中重写不能比父类访问级别更高;子类中不能比父类抛出更多或者更高级的异常。
隐藏
-
子类重新定义父类的静态方法,叫做隐藏。
-
什么叫隐藏父类的静态方法呢?就是说父类的静态方法和子类的静态方法是同时存在的,具体调用的是哪个方法,是要看调用的方法的引用是什么类型的引用,如果是父类型的引用,调用的就是父类的静态方法,如果是子类型的引用,调用的就是子类的静态方法。
区别
这里的区别主要是体现向上转型的时候
- 被隐藏的属性,在子类被强制转换成父类后,访问的是父类中的属性被重写的方法
- 重写在子类被强制转换成父类后,调用的还是子类自身的方法,重写是动态绑定,是受RTTI(run time type identification,运行时类型检查)约束的,隐藏不受RTTI约束,RTTI只针对重写,不针对隐藏。
动态绑定通俗理解
在此之前先介绍两个名词:编译时类型和运行时类型
Java的引用变量有两个类型,一个是编译时类型,一个是运行时类型
-
编译时类型:由声明该变量时使用的类型决定
-
运行时类型:由该变量指向的对象类型决定
举个例子: Father f = new Son(); Son是Father的子类,引用变量f就会出现编译时类型和运行时类型不一致的情况,编译时是Father类型的,运行时是Son类型的,如果编译时类型和运行时类型不一致,会出现所谓的多态
子类其实是一种特殊的父类,因此java允许把一个子类对象直接赋值给一个父类引用变量,无须任何类型转换,。
通过以上的补充,再来理解方法的动态绑定,通俗来讲就是运行的时候才确定对象的类型,并调用适当的方法。
为什么会有动态绑定:在编译期,无法得知真实的对象类型,有人可能会问为什么编译器无法知道真实的对象类型?
回到上边的例子,因为Java允许将把一个子类对象直接赋值给一个父类引用变量,无须任何类型转换,或者被称为向上转型,由系统自动完成。但是编译的时候变量的类型是由申明的类型,也就是由左边类型决定,而右边的对象有可能是它的子类,也可以是他的子类的子类,所以无法得知其真实类型,而到了运行时jvm检查到了右边类型的真实类型, 变量就变成真实的类型。
**那么有没有想过那么为什么允许父类引用指向子类对象的?**先看一个例子:
//父类
public class Father {
public int age;
}
//子类
public class Son extends Father {
public int age;
}
//测试
public class test{
public static void main(String[] args){
Son son = new Son();
}
}
那么构造一个Son,在堆上是这样的:
当然堆对象中肯定还有些其他的内容,如对象头以及方法表等,如上图所示,子类中有父类的内容,**使用super来引用父类的成分,用this来引用当前对象。**看到这很容易理解为什么Father父类引用可以指向Son子类的对象了吧,因为他可以指向Son对象中的Father对象。这个图还能解释一些其他问题:
-
为什么父类对象无法用子类引用
假设子类对象大小2kb,父类对象1kb,引用本该指向2kb的东西,怎么指向1kb?
-
为什么父类对象无法转为子类,Son son = (Son)father,编译不通过
1kb的father对象,无法被2kb的子类引用指向
-
为何Father father=new Son(),father引用无法调用Son对象中特有的方法?
father指向son对象中的father部分的内存,即是说,father只是指向了Son对象中实例的父类实例对象,所以father只能调用父类的方法,而不能调用子类的方法(存储在Son类特有的内存中).
-
为何可以向下强制转换,Father father=new Son();Son son = (Son) father?
Son对象是由Father是转换过来的,它其实是有2kb的内存的,只是father指向2kb中的1kb内存,类型转换时,就可以拿到全部2kb。
跑偏了,接着回到动态绑定,再简单了解下一个方法调用的基本流程:
- 编译器首先查看对象声明的类型和方法名, 假设对象调用 son.eat(param) ,son是声明为Son类的对象,由于重载的存在, 可能有多个名字为eat的方法,然后编译器会列出所有Son类中方法名为 eat的方法以及父类中而且方法名为 eat 的方法(这一步是从类的方法表中搜索),这些方法就是候选方法;
- 通过方法的参数类型,在候选方法中匹配正确方法,这一步叫作“重载解析”,至此, 编译器已经获得需要调用的方法的名字和参数类型。
- 如果是private, static, final修饰的方法或者构造器(constructor), 那么编译器可以准确的指导应该调用哪个方法, 这种方式称为静态绑定(以下详细解释),在编译期间实现了对象和方法的绑定。 与静态绑定相对的, 如果调用方法依赖于对象的实际类型, 在运行时实现对象和方法绑定的叫做动态绑定。当程序运行并且采用动态绑定调用方法时, 虚拟机JVM一定会调用与所引用的对象的实际类型最相符合的那个类的方法。
静态绑定
程序运行前方法已被绑定,即Java中编译期进行的绑定
常见的静态绑定,方法被private,final或者static修饰时,采用静态绑定,举个例子,在多态中,父类引用子类对象后,在编译阶段这个父类引用的子类对象会和父类方法进行绑定,运行阶段,不会进行二次的判断进行动态绑定,不管子类有没有同名参数的方法或者字段,那为什么private,final,static修饰时静态绑定呢?
- private方法屏蔽子类,子类无法继承父类的方法,子类中的同名同参函数和父类中的方法可以认为是两个毫无相关的函数
- final方法可以被继承,无法被重写,根本不会存在同名同参函数的final函数同时在父类子类中,所以无法绑定
- static方法,可以被继承,也不能被重写,子类有,调用子类的,子类没有,调用父类的,如果子类有,那么它在编译阶段就绑定子类的,如果没有,那么它直接直接绑定父类的,而且static方法标准的用法,是直接用类名进行调用最好,所以也无需动态绑定。
再回过头看,为什么只有成员方法有多态性,而成员变量没有多态呢?因为成员方法和静态方法(成员)一样,没有重写,只有隐藏,隐藏不受运行时类型检查约束,所以不会动态绑定,静态绑定的话,编译时绑定在其父类引用类型上了,对应上面表格中第三行,同名成员变量调用父类的。
至于为什么成员变量和成员方法设计成一个隐藏一个重写,通过搜索还没找到原因,个人感觉跟所在的内存位置有关吧,如下表所示,如果有大佬知道,还望告知!
类型 | 内存中位置 |
---|---|
静态方法 | 方法区的静态部分 |
静态成员 | 方法区的静态部分 |
非静态方法 | 方法区的非静态部分 |
非静态变量 | 堆上 |
总结
,通过搜索还没找到原因,个人感觉跟所在的内存位置有关吧,如下表所示,如果有大佬知道,还望告知!
类型 | 内存中位置 |
---|---|
静态方法 | 方法区的静态部分 |
静态成员 | 方法区的静态部分 |
非静态方法 | 方法区的非静态部分 |
非静态变量 | 堆上 |
总结
个人理解,**有重写的就是有动态绑定过程而没有重写的就事静态绑定!**通过以上分析,对运行时多态是不是有更深刻的认识了,由于动态绑定的存在,才有了运行时多态的存在。