Java动态绑定与静态绑定之胡思乱想

Java动态绑定与静态绑定之胡思乱想

之所以写这篇博客,是因为写代码过程中遇到了很奇怪的现象,我觉得只能通过动态绑定与静态绑定来解释,于是,就学习了一下动态绑定与静态绑定的实现原理,这个过程中确实学到了很多,怕以后忘了,所以用博客的形式记录下来。
为啥叫胡思乱想呢,是因为这篇博客主要记录的是我学到的内容和我的一些疑问与解答,并没有很强的逻辑性,所以就叫胡思乱想啦!

Java动态绑定与静态绑定的实现原理

关于实现原理,规范且准确的解释,在这里可以找到。
下面我想用自己的话概括一下,从而使自己更好的理解。

静态绑定实现原理:当初次调用一个使用静态绑定的方法时,java字节码层面是去调用常量池中的某一个常量表,这个常量表描述了这个方法的具体信息(包括详细的类名,函数名称,返回值,存在CONSTANT_Methodref_info这个结构当中)。这时,JVM会首先去加载这个函数所属的类(在这个类之前没有被加载过的情况下),然后,根据方法的具体信息,在对应的类的方法表(Method table)中查找方法代码的具体地址,找到后,直接放入常量池中,放入位置为对应的常量表的位置。这个过程叫做常量池解析。经过解析之后,当第二次调用这个方法时,由于常量池中存的就是地址,所以不需要再次解析,直接利用地址去执行相应的代码块即可!

动态绑定实现原理:采用动态绑定的方法的常量池解析过程与静态绑定类似,不同之处在于,替换相应的常量表的不是代码块的具体地址,而是函数在方法表中的index(根据方法表的设计,子类与父类相同的方法在方法表中会具有相同的index)。等到运行时,再根据调用这个函数的实际实例的类型,由此类型去找到此类对应的方法表,再由方法表去找到实际的代码块的位置。通过多一步的解析,我们就实现了动态绑定!

为了更好理解,放两张图吧:
第一张是常量池的图:
这里写图片描述
在这个常量池中,有28项,每一项都称之为一个常量表,都有相应的结构。共有以下几种类型:
这里写图片描述
而CONSTANT_Methodref_info的结构如下:
这里写图片描述
tag(1 byte)即标记位,class_index(2 bytes)指向一个CONSTANT_Class_info数据项, 这个数据项表示被引用的方法所在的类型(类)name_and_type_index(2 bytes)指向一个CONSTANT_NameAndType_info,这个CONSTANT_NameAndType_info描述的是被引用的方法的名称和描述符。

然后是一个方法表(Method table)的图:
这里写图片描述
从中我们可以看到,子类是会将父类的方法表拷贝下来,然后再增添新的内容,如果重写,则会修改相应的函数的地址。(这也就说明了同样的函数在子类,父类的方法表中的index是一样的)


胡思乱想

知道了动态绑定与静态绑定的工作原理,就开始胡思乱想啦
1.什么样的方法使用静态绑定?什么样的方法使用动态绑定呢?
答:用static,final,private修饰的方法,以及构造方法,都是静态绑定。而其他的方法,就采用动态绑定咯!

2.那对于虚拟机来说,怎么确定使用静态绑定还是动态绑定呢?
答:在Java字节码中,调用函数共有五个指令:invokestatic,invokespecial,invokevirtual,invokeinterface,invokedynamic。而1中提到的应该使用静态绑定的函数都是通过invokestatic和invokespecial这两个来调用的,所以,虚拟机看到这两个指令,就明白应该采用静态绑定啦,而其余的则使用动态绑定!

3.重写和重载分别用的什么绑定呢?
答:显然,重写使用的是动态绑定,而重载,多个函数不同之处在于参数列表,所以,在他们之间,采用静态绑定即可。

4.明白了动态绑定与静态绑定的原理,对你写代码有什么启示啊?
答:可以发现,如果使用静态绑定,在类的加载过程中会比较缓慢,而之后的调用则会变得很快;而如果使用动态绑定的话,在运行时还需要解析,会降低程序的运行速度。所以,我觉得可以在代码中适当的使用final这样的修饰词来提高程序运行时的速度。当然,过于频繁的使用也会带来程序拓展性差,加载时间过长等弊端,还是需要根据实际情况来trade-off的!

5.你前面都在说方法,那你知道变量是静态连接还是动态连接的吗?
答:先说答案,变量采用的是静态连接。这正是开篇讲的写程序时遇到的困扰我的问题,也正是由于此,我才想到了要学习静态绑定与动态绑定。下面,让代码来说话。
先看一个很奇妙的现象:
这里写图片描述
这里写图片描述
这里写图片描述
以上代码的输出如下:
这里写图片描述
我们发现,直接访问属性value,访问到的是父类的;而访问方法,又访问到的是子类的。这当时给我造成了极大的困扰,实际,这个现象可以用动态绑定与静态绑定的知识来解答,那就是:属性采取的是静态绑定!而sayHello()这个方法采用了动态绑定!正是这个原因,当你将a声明为A类型时,在编译阶段,value就已经绑定为了类A的value,而对于sayHello()来说,则采取的动态绑定,所以,到运行时,发现实际类型是A1,自然就运行的是A1的sayHello()。

基于属性是静态绑定这个事实,也就可以很容易的解释:在继承中,属性只能被隐藏,而方法可以被覆盖或隐藏(静态的方法是隐藏)。
覆盖的意思就是说你把子类看成父类,访问到的依然是子类的;而隐藏的话,你把子类看成父类,访问到的就是父类的了。
将上面的代码略微改动之后如下:
这里写图片描述
这里写图片描述
这里写图片描述
这里写图片描述
我们在子类A1中使用int型的value去覆盖了父类的String型value,并重写(覆盖)了sayHello()方法,输出结果与我们之前所述一致。

6.请问,在在父类,有个int value,你可以在子类中定义 boolean value,但是,如果父类中有个public int get(),你却不能在子类中定义public boolean get()。这是为什么?
答:根据5中的分析,由于变量采用静态绑定,并且是隐藏,所以,子类可以完全自由的用和父类变量一样的变量名字,并且采用不同的修饰符,静态绑定会确保正确性。而对于方法,子类是会继承的,并且采取动态绑定(覆盖),如果你既有public int get(),又有public boolean get(),那当你调用get()时,怎么知道调用哪个呢?所以这样是不允许的!

补充(2018.5.4):上面的回答从底层实现的角度阐释了为什么不允许通过不同的返回值类型来进行重载或重写,今天在读《Thinking in java》这本书时,看到了设计层面的回答:如果允许通过不同的返回值进行重载或重写,如果你在调用函数时左端使用相应的类型来接收返回值的话,这样依然可以分辨调用的是哪个函数,如下:

int say(){
    return 1;
}
String say(){
    return "hello";
}
int x=say();
String y=say();

但是,在某些情况下,我们可能只想执行一下函数,而不关心返回值,这时,我们可能这样写代码:

int say(){
    return 1;
}
String say(){
    return "hello";
}
say();

当这种情况出现时,就无法分辨应该调用哪个say()了,因此,不允许通过不同的返回值类型来进行重载或重写。

7.通过学习静态绑定与动态绑定的原理,你也应该能很准确的解释 为什么你声明为哪个类,就只能执行这个类中拥有的方法(因为常量池解析时是根据你声明的类型的方法表来做的,而不是根据实际类型!),并且,子类没有重写的话执行的是父类的方法(因为方法表的继承呀!)。

8.加深理解的一段代码:
这里写图片描述
这里写图片描述
这个结果,一不小心就会搞错,很容易会觉得应该输出的是 Son-s1() para-char。但你仔细一想,父类中都没有接收char参数的函数,编译是怎么通过的呢?原来,是因为编译器因为找不到char而做了让步,因为char可以隐式转为int,所以,编译器就找了这么一个“凑合的方法”,这样,在常量池解析时,就变成了调用的是参数为int的方法,而运行时,子类又没有重写这个参数为int的方法,所以,就调用的父类的啦!

9.对动态绑定和静态绑定的一个总结:
这里写图片描述
(注:此图来源于知乎)

猜你喜欢

转载自blog.csdn.net/qq_41854763/article/details/79795866
今日推荐