hello啊,各位观众姥爷们!!!本baby今天又来报道了!哈哈哈哈哈嗝🐶
程序员各种工具大全
我们先把结论放在前面:
- 重载是"编译时"行为 ,依赖静态分派。编译器在编译阶段就能确定要调用哪个方法。
- 多态是"运行时"行为 ,依赖动态分派。JVM 在运行期间才能确定要调用哪个方法。
一、重载的底层原理:静态分派
1. 什么是静态分派?
分派指的是确定调用哪个方法的过程。静态分派是指在程序编译期就确定方法调用版本的分派动作。重载就是最典型的静态分派。
2. 核心依据:静态类型
静态分派的核心是变量的"静态类型"(也叫外观类型),而不是变量的"实际类型"。
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, guy!");
}
public void sayHello(Man guy) {
System.out.println("Hello, gentleman!");
}
public void sayHello(Woman guy) {
System.out.println("Hello, lady!");
}
public static void main(String[] args) {
Human man = new Man(); // 静态类型 = Human,实际类型 = Man
Human woman = new Woman(); // 静态类型 = Human,实际类型 = Woman
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man); // 输出 "Hello, guy!"
sr.sayHello(woman); // 输出 "Hello, guy!"
}
}
在上面的代码中:
Human man = new Man();这里的man变量的静态类型 是Human,而实际类型 是Man。- 在调用
sayHello()方法时,编译器 在编译阶段,只关心参数的静态类型 (即Human)。 - 因此,编译器会将
sr.sayHello(man)和sr.sayHello(woman)都编译为调用sayHello(Human guy)方法的字节码指令。
3. 底层实现
- 编译后的
.class文件中,方法调用指令(如invokevirtual)的参数已经是一个符号引用 ,这个符号引用明确指向了sayHello(Human)方法。 - JVM 在执行时,只是简单地执行这条已经确定好的指令,没有任何选择的余地。
总结:重载的本质是编译器根据方法的签名(方法名+参数列表)和传入参数的静态类型,在编译期就唯一确定了要调用的目标方法。
二、多态的底层原理:动态分派
1. 什么是动态分派?
动态分派是指在程序运行期 根据对象的实际类型来确定方法执行版本的分派过程。方法的重写就是最经典的动态分派。
2. 核心依据:实际类型
动态分派的核心是变量的**"实际类型"**。
java
public class DynamicDispatch {
static abstract class Animal {
public abstract void speak();
}
static class Dog extends Animal {
@Override
public void speak() { System.out.println("Wang!"); }
}
static class Cat extends Animal {
@Override
public void speak() { System.out.println("Miao!"); }
}
public static void main(String[] args) {
Animal a1 = new Dog(); // 实际类型 = Dog
Animal a2 = new Cat(); // 实际类型 = Cat
a1.speak(); // 输出 "Wang!"
a2.speak(); // 输出 "Miao!"
a1 = new Cat();
a1.speak(); // 输出 "Miao!"
}
}
3. 底层实现:方法表与 invokevirtual 指令
这是整个机制最精妙的部分。
-
方法表:
- JVM 会为每个类维护一个方法表,其中列出了该类型所有方法的实际入口地址。
- 子类的方法表中,对于重写的方法,会替换成子类自己的实现入口;对于未重写的方法,则保留父类的入口。
- 例如:
Animal类的方法表:speak() -> Animal.speak()Dog类的方法表:speak() -> Dog.speak()Cat类的方法表:speak() -> Cat.speak()
-
invokevirtual指令的执行过程 :当 JVM 遇到
a1.speak()这条字节码指令(invokevirtual)时,它会执行以下步骤:- 找到操作数栈顶元素所指向的对象的实际类型 。假设
a1实际指向一个Dog对象。 - 在实际类型(
Dog)的方法表中查找speak方法。如果找到,就跳转到其入口地址执行。 - 如果没找到,则按照继承关系从下往上(
Dog->Animal)依次查找其父类的方法表。
这个过程是在运行期间 完成的。当
a1的实际类型从Dog变为Cat时,下一次执行a1.speak(),JVM 就会去Cat的方法表中查找,从而调用Cat.speak()。 - 找到操作数栈顶元素所指向的对象的实际类型 。假设
总结:多态的本质是 JVM 在运行时,通过检查对象的实际类型,在其方法表中动态查找并调用正确的方法实现。
要点对比
| 特性 | 重载 | 多态 |
|---|---|---|
| 英文名 | Overload | Override |
| 发生阶段 | 编译时 | 运行时 |
| 分派类型 | 静态分派 | 动态分派 |
| 核心依据 | 方法的参数列表 + 参数的静态类型 | 对象的实际类型 |
| JVM指令 | 编译后指令的目标方法已确定 | 使用 invokevirtual,执行时需要查找方法表 |
| 本质 | 编译器行为,是"语法糖" | JVM 机制,是面向对象继承的基石 |
简单来说,重载是"写代码时"就定死的,而多态是"程序运行时"才活起来的。理解它们的底层区别,对于编写高效、正确的 Java 程序至关重要。
程序员各种工具大全
