重写和重载
重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同而且参数列表也相同的方法之间的关系 。
public class OneOverride {
//=========================
// 这两个方法构成重载
public void show(){
}
public void show(String str) {
}
//===============================
}
/**
* 重写父类方法
*/
public class OneOverriderChilden extends OneOverride{
public void show(String str) {
}
}
java 虚拟机识别方法的关键在于类名,方法名以及方法描述符(method descriptor),方法描述符,它是由方法的参数类型以及返回类型所构成。
方法调用
Java中的方法调用分为两大类:
1、解析调用(Resolution): 在类加载的解析阶段,会把其中的一部分符号引用转化为直接引用。 前提是:方法在程序运行之前,就有一个可确定的调用版本,且该版本在运行期不可变。即“编译期可知,运行期不变”,符合这个要求的主要包括静态方法和私有方法两大类,前者与类型直接关联,后者外部无法调用,因此无法通过继承重写。
2、分派调用(Dispatch):又分为 “静态分派” “动态分派” “多分派” “单分派”。在运行期间才能确定调用方法的版本。
解析调用
jvm 字节码调用指令
jvm 提供了5条调用方法的字节码指令,分别是 :
invokestatic: 调用静态方法
invokespecial: 调用实例构造器方法、私有方法和父类方法
invokevirtual:调用所有的虚方法
invokeinterface:调用接口方法,会在运行时确定一个实现此接口的对象
invokedynamic: 先在运行时动态解析出调用点限定符所引用的方法,然后再执行
其中 invokestatic 和 invokespecial 在类加载阶段会把方法的符号引用解析成直接引用(内存地址入口),这类方法也称为非虚方法。
注意的是: final方法虽然是用invokevirtual来调用的,但是因为它无法被覆盖,是唯一的,不需动态解析的,所以它也是非虚方法。
来看个例子
public class StaticResolution {
public static void sayHello(){
System.out.println("hello world");
}
public static void main(String[] args) {
StaticResolution.sayHello();
}
}
这里调用了静态方法,那么使用 javap -v XX应该会使用 invokestatic 。
[root@iZm5e7bivgszquxjh18i39Z jvm测试]# javap -v StaticResolution
Classfile /home/jvm测试/StaticResolution.class
Last modified Mar 4, 2020; size 504 bytes
MD5 checksum f2bbab54fb03714e2332b782be397bfb
Compiled from "StaticResolution.java"
public class StaticResolution
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#17 // java/lang/Object."":()V
#2 = Fieldref #18.#19 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #20 // hello world
#4 = Methodref #21.#22 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Methodref #6.#23 // StaticResolution.sayHello:()V
#6 = Class #24 // StaticResolution
#7 = Class #25 // java/lang/Object
#8 = Utf8
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 sayHello
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 SourceFile
#16 = Utf8 StaticResolution.java
#17 = NameAndType #8:#9 // "":()V
#18 = Class #26 // java/lang/System
#19 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#20 = Utf8 hello world
#21 = Class #29 // java/io/PrintStream
#22 = NameAndType #30:#31 // println:(Ljava/lang/String;)V
#23 = NameAndType #12:#9 // sayHello:()V
#24 = Utf8 StaticResolution
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/System
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Utf8 java/io/PrintStream
#30 = Utf8 println
#31 = Utf8 (Ljava/lang/String;)V
{
public StaticResolution();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public static void sayHello();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 4: 0
line 5: 8
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #5 // Method sayHello:()V
3: return
LineNumberTable:
line 8: 0
line 9: 3
}
SourceFile: "StaticResolution.java"
静态分派
在讲静态分派之前我们需要知道静态类型和动态类型,例如有以下程序 :
public class StaticDispatch {
static abstract class Human{}
static class Man extends Human{}
static class Woman extends Human{}
public void sayHello(Human guy){
System.out.println("Hello human");
}
public void sayHello(Man guy){
System.out.println("Hello man");
}
public void sayHello(Woman guy){
System.out.println("Hello woman");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
输出 :
Hello human
Hello human
上面的
Human man = new Man();
这里 “Human”是 man变量的 静态类型 (Static Type) 或者叫 外观类型(Apparent Type)而后面的 “Man” 则是 man 变量的 实际类型(Actual Type)。静态类型都实际类型在程序中都可以发生变化,** 区别在于静态类型的变化仅仅是在使用时发生,而其本身的静态类型并不发生改变。**
什么意思呢?就是 man 这个对象在被传作参数还是调用方法的时候,我们依然为会认为它是“Human”只有使用的时候它才是“Man”。
重载与静态分配
有三个关键点需要知道 :
静态类型在编译期可知,而动态类型只有实际运行时能够获知。
虚拟机是通过参数静态类型作为重载的判定依据
静态分派发生在编译阶段
但是重载有时候也会选择困难--我应该选择哪个重载方法,例如 :
public class Overload {
public static void sayHello(Object obj){
System.out.println("Hello object");
}
public static void sayHello(int arg){
System.out.println("Hello int");
}
public static void sayHello(long arg){
System.out.println("Hello long");
}
public static void sayHello(Character arg){
System.out.println("Hello character");
}
public static void sayHello(char ...arg){
System.out.println("Hello char ...");
}
public static void sayHello(Serializable arg){
System.out.println("Hello Serializable ");
}
public static void main(String[] args) {
sayHello('a');
}
}
输出 :
Hello int
重载的规则:
自身类型匹配
是否是基本类型,是,考虑自动装拆箱
形参的继承关系与重载方法是否匹配
变长参数匹配
另外以下也是静态分配 :
public class ResolutionAndDispatch{
static void sayHello(int arg){
System.out.println("Hello int");
}
static void sayHello(char arg){
System.out.println("Hello char");
}
public static void main(String[] args){
ResolutionAndDispatch.sayHello('a’);
}
}
分派调用
分派调用揭示了OOP多态性的一些最基本的体现。“重载”和“重写”,就是其中之一。
如下例子 :
public class DynamicDispatch {
static abstract class Human{
protected abstract void sayHello();
}
static class Man extends Human{
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}
static class Woman extends Human{
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
可以看到子类重写了父类的方法。
[root@iZm5e7bivgszquxjh18i39Z jvm测试]# javap -v DynamicDispatch
Classfile /home/jvm测试/DynamicDispatch.class
Last modified Mar 4, 2020; size 514 bytes
MD5 checksum 7c19cd382f0b914eac869cb42608314f
Compiled from "DynamicDispatch.java"
public class DynamicDispatch
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#22 // java/lang/Object."":()V
#2 = Class #23 // DynamicDispatch$Man
#3 = Methodref #2.#22 // DynamicDispatch$Man."":()V
#4 = Class #24 // DynamicDispatch$Woman
#5 = Methodref #4.#22 // DynamicDispatch$Woman."":()V
#6 = Methodref #12.#25 // DynamicDispatch$Human.sayHello:()V
#7 = Class #26 // DynamicDispatch
#8 = Class #27 // java/lang/Object
#9 = Utf8 Woman
#10 = Utf8 InnerClasses
#11 = Utf8 Man
#12 = Class #28 // DynamicDispatch$Human
#13 = Utf8 Human
#14 = Utf8
#15 = Utf8 ()V
#16 = Utf8 Code
#17 = Utf8 LineNumberTable
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 SourceFile
#21 = Utf8 DynamicDispatch.java
#22 = NameAndType #14:#15 // "":()V
#23 = Utf8 DynamicDispatch$Man
#24 = Utf8 DynamicDispatch$Woman
#25 = NameAndType #29:#15 // sayHello:()V
#26 = Utf8 DynamicDispatch
#27 = Utf8 java/lang/Object
#28 = Utf8 DynamicDispatch$Human
#29 = Utf8 sayHello
{
public DynamicDispatch();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class DynamicDispatch$Man
3: dup
4: invokespecial #3 // Method DynamicDispatch$Man."":()V
7: astore_1
8: new #4 // class DynamicDispatch$Woman
11: dup
12: invokespecial #5 // Method DynamicDispatch$Woman."":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method DynamicDispatch$Human.sayHello:()V
20: aload_2
21: invokevirtual #6 // Method DynamicDispatch$Human.sayHello:()V
24: new #4 // class DynamicDispatch$Woman
27: dup
28: invokespecial #5 // Method DynamicDispatch$Woman."":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method DynamicDispatch$Human.sayHello:()V
36: return
LineNumberTable:
line 20: 0
line 21: 8
line 22: 16
line 23: 20
line 24: 24
line 25: 32
line 26: 36
}
SourceFile: "DynamicDispatch.java"
InnerClasses:
static #9= #4 of #7; //Woman=class DynamicDispatch$Woman of class DynamicDispatch
static #11= #2 of #7; //Man=class DynamicDispatch$Man of class DynamicDispatch
static abstract #13= #12 of #7; //Human=class DynamicDispatch$Human of class DynamicDispatch
0~15 在做准备动作 我们看到调用了两次 invokespecial 是调用了实例构造器 构造了man 和woman两个实例,并且把他们的引用放在1、2个局部变量表Slot中接下来的16~21,16和20两句aload_1和aload_2 把创建的对象的引用压到栈顶,这两个对象是将要执行的方法sayHello()的执行者,称作接受者(Receiver) 17和21两句的方法调用指令 和参数 都是一样的,但是最终执行的目标方法不同,原因就是invokevirtual指令的多态查找
虚方法调用
java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用都会被编译成 invokeinterface 指令,这均属于java虚拟机中的需方法调用。
**java 虚拟机采取了一种空间换时间的策略来实现动态绑定。**它为每个类生成一个方法表,用以快速定位目标方法。
方法表
方法表满足两个特性 :
子类表中包含父类表中的所有方法
子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同
在执行过程中,java虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。
思考一下假如我们如果不使用方法表,我们就需要先去收集然后再查找目标方法了,但是即使使用了方法表还有没优化的空间呢?即时编译(JIT)还拥有另外两种性能更好的优化手段 : 内联缓存 和方法内联
内联缓存
它能够缓存虚方法中调用者的动态类型,以及该类型对应的目标方法。 在之后的执行过程中,如果碰到已缓存的类型,直接在缓存中找到对应的目标方法,没有找到,那么就会去方法表中寻找。
方法内联
后续讲解
补充
查看汇编后的java class
javap -v xxx
参考资料
https://tobiaslee.top/2017/02/14/Override-and-Overload/
《深入JVM》课程