重写和重载
重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同而且参数列表也相同的方法之间的关系 。
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."<init>":()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 <init> #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 // "<init>":()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."<init>":()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."<init>":()V #2 = Class #23 // DynamicDispatch$Man #3 = Methodref #2.#22 // DynamicDispatch$Man."<init>":()V #4 = Class #24 // DynamicDispatch$Woman #5 = Methodref #4.#22 // DynamicDispatch$Woman."<init>":()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 <init> #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 // "<init>":()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."<init>":()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."<init>":()V 7: astore_1 8: new #4 // class DynamicDispatch$Woman 11: dup 12: invokespecial #5 // Method DynamicDispatch$Woman."<init>":()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."<init>":()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》课程