大多数人或许都知道方法重写是运行时多态,方法重载是编译时多态,那你知道为什么其中的原因吗?希望这篇文章能够解答你的疑惑。
目录
2.invokespecial: 调用对象的private方法、构造方法,以及使用 super 关键字调用父类实例的方法、构造方法,以及所实现接口的默认方法。
invokestatic和invokespecial是编译时多态,具体原因如下:
重载(Overloading)
重载指在同一个类中定义多个同名但参数不同的方法。
主要特点:
- 方法名必须相同
- 参数列表必须不同(参数类型、个数、顺序)
- 返回值类型可以相同也可以不同
- 发生在同一个类中
示例代码如下
public class Calculator {
// 两个整数相加
public int add(int a, int b) {
return a + b;
}
// 三个整数相加
public int add(int a, int b, int c) {
return a + b + c;
}
// 两个浮点数相加
public double add(double a, double b) {
return a + b;
}
}
重写(Overriding)
重写指子类重新定义父类中已有的方法。
主要特点:
- 方法名必须相同
- 参数列表必须相同
- 返回值类型必须相同或是父类返回值的子类
- 访问修饰符不能比父类更严格
- 不能抛出比父类方法更多的异常
示例代码如下
public class StaticPolymorphism {
public void display(int num) {
System.out.println("整数:" + num);
}
public void display(String text) {
System.out.println("字符串:" + text);
}
public static void main(String[] args) {
StaticPolymorphism demo = new StaticPolymorphism();
demo.display(100); // 编译时就确定调用第一个方法
demo.display("测试"); // 编译时就确定调用第二个方法
}
}
运行时多态(动态多态)
主要通过继承和方法重写实现,在运行时才确定要调用的方法。
特点:
1. 在运行时期才确定调用哪个方法
- 基于对象的实际类型(动态绑定)
- 需要继承和方法重写
- 更灵活,支持多态性
想要解释到运行的多态性就得从方法调用得字节码出发了
方法调用的本质是通过字节码指令的执行,能在栈上创建栈帧,并执行调用方法中的字节码执行。以invoke开头的字节码指令的作用是执行方法的调用。
1.调用study方法,会执行invokestatic指令,Java虚拟机找到#2对应的方法,也就是study方法,创建栈帧。
2.eat和sleep方法也是类似的处理方式,就可以执行方法里的字节码指令了
在JVM中,一共有五个字节码指令可以执行方法调用
1. invokestatic:用于调用静态方法。
特点:
- 最简单的方法调用
- 在编译期间就可以确定目标方法
- 不需要动态绑定
- 性能最好
public class StaticMethodExample {
public static void staticMethod() {
System.out.println("静态方法");
}
public static void main(String[] args) {
staticMethod(); // 使用invokestatic调用
}
}
2.invokespecial: 调用对象的private方法、构造方法,以及使用 super 关键字调用父类实例的方法、构造方法,以及所实现接口的默认方法。
public class SpecialMethodExample {
private void privateMethod() {
System.out.println("私有方法");
}
public SpecialMethodExample() {
// 构造器调用使用invokespecial
}
public void callPrivate() {
privateMethod(); // 使用invokespecial调用
}
}
class Child extends SpecialMethodExample {
public void callSuper() {
super.callPrivate(); // 使用invokespecial调用父类方法
}
}
3. invokevirtual:用于调用对象的实例方法。
特点:
- 最常用的方法调用指令
- 支持动态分派
- 在运行时根据对象的实际类型确定要调用的方法
public class VirtualMethodExample {
public void instanceMethod() {
System.out.println("实例方法");
}
public static void main(String[] args) {
VirtualMethodExample obj = new VirtualMethodExample();
obj.instanceMethod(); // 使用invokevirtual调用
}
}
4. invokeinterface:用于调用接口方法。
特点:
- 用于调用接口中的方法
- 需要运行时动态查找具体实现
- 性能相对较差
- 支持动态分派
interface MyInterface {
void interfaceMethod();
}
class InterfaceImpl implements MyInterface {
@Override
public void interfaceMethod() {
System.out.println("接口方法实现");
}
}
public class InterfaceMethodExample {
public static void main(String[] args) {
MyInterface obj = new InterfaceImpl();
obj.interfaceMethod(); // 使用invokeinterface调用
}
}
5. invokedynamic:用于支持动态语言特性。
特点:
- Java 7引入
- 用于实现动态语言支持
- 用于lambda表达式实现
- 最灵活但性能开销最大
public class LambdaExample {
public static void main(String[] args) {
// Lambda表达式会使用invokedynamic
Runnable r = () -> System.out.println("Lambda表达式");
r.run();
}
}
性能比较
从性能角度排序(从快到慢):
invokestatic>invokespecial>invokevirtual> invokeinterface>invokedynamic
invokestatic和invokespecial是编译时多态,具体原因如下:
从方法解析时机出发:
- 编译器在编译时就能确定要调用哪个方法
- 生成的字节码中直接包含目标方法的引用
- 不需要运行时再进行方法查找
从静态方法的特性出发:
- 不依赖对象实例
- 不能被重写(Override)
- 不支持动态分派
静态绑定
1、编译期间,invoke指令会携带一个参数符号引用,引用到常量池中的方法定义。方法定义中包含了类名 + 方法名 + 返回值 + 参数。
2、在方法第一次调用时,这些符号引用就会被替换成内存地址的直接引用,这种方式称之为静态绑定。
静态绑定适用于处理静态方法、私有方法、或者使用final修饰的方法,因为这些方法不能被继承之后重写。如invokestati、invokespecial、final修饰的invokevirtual。
动态绑定
对于非static、非private、非final的方法,有可能存在子类重写方法,那么就需要通过动态绑定来完成方法地址绑定的工作。比如在这段代码中,调用的其实是Cat类对象的eat方法,但是编译完之后虚拟机指令中调用的是Animal类的eat方法,这就需要在运行过程中通过动态绑定找到Cat类的eat方法,这样就实现了多态。
动态绑定是基于方法表来完成的,invokevirtual使用了虚方法表(vtable),invokeinterface使用了接口方法表(itable),整体思路类似。所以接下来使用invokevirtual和虚方法表来解释整个过程。
每个类中都有一个虚方法表,本质上它是一个数组,记录了方法的地址。子类方法表中包含父类方法表中的所有方法;子类如果重写了父类方法,则使用自己类中方法的地址进行替换。
产生invokevirtual调用时,先根据对象头中的类型指针找到方法区中InstanceClass对象,获得虚方法表。再根据虚方法表找到对应的对方,获得方法的地址,最后调用方法。
好了,现在重新回到重写的层面,子类重写了父类的方法,只有在运行时经过虚方法表这一系列的动态查找动作才能确定最后要执行的方法,才去创建对应的栈帧,调用具体方法的字节码,并不是在编译时就确定要调用哪个方法,所以重写才是运行时的多态性。
隐藏和重写
到这个时候可能会有些疑问,那重写父类的静态方法不就是编译时的多态?调用静态方法是编译时的多态,重写是运行时的多态,这样不就出现冲突了?
这种情况叫做静态方法隐藏(Static Method Hiding),而不是重写(Override)。
简单代码如下:
class Parent {
public static void staticMethod() {
System.out.println("父类静态方法");
}
}
class Child extends Parent {
// 这不是重写,而是静态方法隐藏
public static void staticMethod() {
System.out.println("子类静态方法");
}
}
public class StaticMethodTest {
public static void main(String[] args) {
Parent p = new Child();
p.staticMethod(); // 输出:父类静态方法
Child c = new Child();
c.staticMethod(); // 输出:子类静态方法
}
}