方法调用
1. 重载与重写
1.1 Java虚拟机
虚拟机识别方法时主要根据类名,方法名和方法描述符(参数类型和返回值类型).
如果出现类名,方法名和方法描述符相同的方法,Java虚拟机在类加载的验证阶段报错.
1.2 Java语言
在同一个类中,方法名称相同,参数类型不同的方法称之为重载.(返回值类型不影响方法的重载)
如果在子类中定义了与父类非私有的,方法名相同,参数列表不同的方法。那么在子类中这两个方法构成了重载。
如果在子类中定义了与父类非私有的,方法名相同,参数列表相同的方法:
如果两个方法都是静态的,则子类隐藏了父类的静态方法.
如果两个方法都不是静态的,且都不是私有的,则子类重写了父类的方法。
如果出现类名,方法名和参数类型相同,但是返回值类型不同的方法,Java编译器会报错.
1.3 小知识
这个限制可以通过字节码工具绕开。也就是说,在编译完成之后,我们可以再向 class 文件中添加方法名和参数类型相同,而返回类型不同的方法。当这种包括多个方法名相同、参数类型相同,而返回类型不同的方法的类,出现在 Java 编译器的用户类路径上时,它是怎么确定需要调用哪个方法的呢?当前版本的 Java 编译器会直接选取第一个方法名以及参数类型匹配的方法。并且,它会根据所选取方法的返回类型来决定可不可以通过编译,以及需不需要进行值转换等。
1.4 方法的调用
重载方法在编译期间就可以确定.编译器根据传入参数的静态类型(不是实际类型)选取重载方法.
一般分为三个步骤:
- 不考虑基本类型的拆装箱,以及可变参数的情况.
- 第一阶段未找到合适的方法,则允许在自动拆装箱,但不允许可变参数的情况选取重载方法.
- 第二阶段未找到合适的方法,则允许在自动拆装箱,允许可变参数的情况选取重载方法.
Java编译器在同一阶段选取重载方法时根据继承关系选取最为贴近的方法。
比如一个方法的参数类型为Object, 另一个为String.当参数同时符合两种方法的参数类型时,优先选取String类型的方法.
1.5 代码示例
1 package com.skd.jvm.method; 2 3 public class OverLoad 4 { 5 public static void main(String[] args) 6 { 7 Son son = new Son(); 8 // 重载父类的实例方法 9 son.say("hello"); 10 // 重载父类的静态方法 11 Son.out(1,2); 12 // 调用子类的静态方法,隐藏父类的静态方法 13 Son.out(123); 14 // 根据继承关系选择最为贴近的方法 15 // 因为String是Object的子类,所以选择String参数类型的方法 16 son.say(null); 17 18 } 19 20 } 21 22 class Father 23 { 24 public static void out(int i) 25 { 26 System.out.println("Father:" + i); 27 } 28 29 public static void out(String i) 30 { 31 System.out.println("Father:" + i); 32 } 33 34 public void say(int msg) 35 { 36 System.out.println("Father int :" + msg); 37 } 38 } 39 40 class Son extends Father 41 { 42 public static void out(int i) 43 { 44 System.out.println("Son:" + i); 45 } 46 47 public static void out(int i, int j) 48 { 49 System.out.println("Son:" + i + j); 50 } 51 52 public void say(String msg) 53 { 54 System.out.println("son String :" + msg); 55 } 56 57 public void say(Object msg) 58 { 59 System.out.println("son Object :" + msg); 60 } 61 }
运行结果:
2. 五种方法调用指令
- invokestatic:用于调用静态方法。
- invokespecial:用于调用私有方法,构造器,以及使用super关键字调用父类实例方法或者构造方法,和所实现接口的默认方法。
- invokevirtual:用于调用非私有的实例方法。
- invokeinterface:用于调用接口方法。
- invokedynamic:用于调用动态方法。
2.1 方法的解析
虚拟机规范规定方法在通过字节码指令调用之前,必须对他们所使用的符号引用进行解析。
虚拟机可以根据需要决定是在类加载时对常量池的符号引用进行解析还是在符号引用调用之前进行解析。
除了invokedynamic指令以外,其他操作符号引用的字节码指令对调用的符号引用解析后会对结果进行缓存(解析一次,在运行时常量池记录直接引用,并把常量标识为已解析状态),避免解析动作重复进行。
invokedynamic指令调用的方法每次调用时都必须进行一次解析动作。
2.2 可以在类加载阶段解析的方法
在类加载时可以进行解析的符号引用必须在程序真正运行之前就有一个可确定的调用版本。并且该版本在运行期间不可变。
符号上述要求的方法一般为通过invokestatic和invokespecial指令调用的方法。
这类方法称之为非虚方法(包括final方法),其他方法称之为虚方法。
2.3 运行时动态解析虚方法
虚方法的版本选择需要在运行时根据调用对象的实际类型在其方法的元数据中搜索合适的目标方法。
基于性能考虑,虚拟机为每个类在方法区中建立一个虚方法表(接口中有接口方法表)。
方法表本质是一个数组,每一个数组元素指向一个当前类或者祖先类的非私有的实例方法。(私有方法和静态方法在编译期间就可以确定方法调用的版本了)
方法表中存放各个方法的实际入口,如果子类重写了某个方法,则方法表中的地址会替换为子类方法实现版本的入口,否则指向父类方法版本的实现入口。
具有相同签名的方法在方法表中应当具有一样的索引序号。
方法表时动态绑定的稳定优化手段。除此之外,还有两种较为激进的优化手段:内联缓存和方法内联。
内联缓存:缓存对象的动态类型即目标方法。
方法内联: