Java是面向对象的编程语言,面向对象的三大特点是封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。
这里我们详细谈论一下多态的实现。
多态是指同一个方法调用可以在不同的对象上表现出不同的行为。多态分为编译时多态(方法重载)和运行时多态(方法重写)。
这里需要简单介绍一下方法调用:
方法调用不等于方法被执行,方法调用阶段的唯一任务是确定方法的版本。
在Java文件中,我们通过.调用方法。
scss
//Test类名
//test实例名
Test.staticMethod()//直接调用本类的静态方法可以省略类名
test.method()//直接调用本类的实例方法可以省略this
但是,这只是代码而已,它怎么落诸实现呢?
首先,代码会被编译,但是,Java的编译过程不涉及连接步骤,编译好的Class文件中存在的仅仅是对于方法的符号引用,而不是方法在实际运行时的内存布局的入口地址,也就是直接引用。
方法的直接引用需要在类加载乃至运行时才能确定。
类加载时确定方法
这一类方法满足如下特点
- 编译器可知。
- 运行时不可变。
主要有静态方法和私有方法。静态方法和类绑定,私有方法外部不可访问。
私有方法这里还需要解释一下
csharp
public class F {
private void show(){
System.out.println("F show;");
}
public static void main(String[] args) {
F f = new S();
f.show();
}
}
class S extends F{
public void show(){
System.out.println("S show;");
}
}
猜测一下这里的结果,如果F.show()是private,这里结果就会是 System.out.println("F show;");,反过来,如果是public,就是S的 System.out.println("S show;");
为什么呢?为什么这里不会出现多态性呢?
因为,private方法声明的目的就是,表明,我只想要调用我本类自己声明的方法,它直接通过修饰符阻挠了这个方法被修改的可能。
这里子类S虽然也声明了一个show(),但是它看不到父类的show(),和父类的show()也没有任何关系。纯粹就只是一个重名,虽然这并不太好。
从编译的层面,静态方法会调用invokestatic,私有方法的调用会使用invokespecial,当JVM看到这个之后,就不会再触发动态分派了。
对应到上面,对应的字节码指令是:
csharp
0 new #5 <com/gitee/S>
3 dup
4 invokespecial #6 <com/gitee/S.<init> : ()V>
7 astore_1
8 aload_1
9 invokespecial #7 <com/gitee/F.show : ()V>
12 return
关注命令9,invokespecial。
只要能够被invokestatic and invokespecial指令调用,就可以在解析阶段确定唯一的调用版本。
静态方法、私有方法、实例构造器、父类方法四种方法都满足上述条件。
另外,被final修饰的方法虽然使用invokevirtual修饰,但是也能够在解析过程中确定唯一的调用版本。
以上五种方法被统称为非虚方法。其余的方法则是虚方法。
JVM的方法分派调用是理解Java多态性的核心。让我详细解释整个过程。
分派调用概述
分派调用决定了在方法调用时应该执行哪个方法版本。主要分为两大类:
- 静态分派 :编译期确定,基于静态类型
- 动态分派 :运行期确定,基于实际类型
1. 静态分派(方法重载)
原理
在编译期根据方法的声明类型 和参数类型确定调用目标。
代码示例
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) {
StaticDispatch dispatch = new StaticDispatch();
Human man = new Man(); // 静态类型=Human,实际类型=Man
Human woman = new Woman(); // 静态类型=Human,实际类型=Woman
dispatch.sayHello(man); // 输出: Hello, human!
dispatch.sayHello(woman); // 输出: Hello, human!
// 编译期就确定了调用 Human 版本的方法
}
}
编译期解析过程
scss
// 编译器的工作:方法重载解析
public class CompilerOverloadResolution {
public void resolveOverload(MethodCall call) {
// 1. 收集所有候选方法
List<Method> candidates = collectCandidateMethods(
call.getReceiverType(),
call.getMethodName(),
call.getArgumentTypes()
);
// 2. 根据参数类型匹配度排序
candidates.sort((m1, m2) -> {
return compareParameterCompatibility(
m1.getParameterTypes(),
m2.getParameterTypes(),
call.getArgumentTypes()
);
});
// 3. 选择最具体的方法
Method selected = candidates.get(0);
// 4. 生成对应的字节码指令
generateBytecode(selected);
}
}
2. 动态分派(方法重写)
原理
在运行期根据对象的实际类型确定调用目标。
代码示例
scala
public class DynamicDispatch {
static abstract class Animal {
public abstract void speak();
}
static class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof!");
}
}
static class Cat extends Animal {
@Override
public void speak() {
System.out.println("Meow!");
}
}
public static void main(String[] args) {
Animal dog = new Dog(); // 静态类型=Animal,实际类型=Dog
Animal cat = new Cat(); // 静态类型=Animal,实际类型=Cat
dog.speak(); // 输出: Woof! (运行期决定)
cat.speak(); // 输出: Meow! (运行期决定)
}
}
字节码分析
yaml
// 对应的字节码
public static void main(java.lang.String[]);
Code:
0: new #2 // class DynamicDispatch$Dog
3: dup
4: invokespecial #3 // Method DynamicDispatch$Dog."<init>":()V
7: astore_1
8: new #4 // class DynamicDispatch$Cat
11: dup
12: invokespecial #5 // Method DynamicDispatch$Cat."<init>":()V
15: astore_2
// 关键:两个都是 invokevirtual
16: aload_1
17: invokevirtual #6 // Method Animal.speak:()V
20: aload_2
21: invokevirtual #6 // Method Animal.speak:()V
24: return
3. 单分派 vs 多分派
单分派(Java的实现)
根据一个宗量(接收者类型)进行分派。
多分派(如CLOS语言)
根据多个宗量进行分派。
typescript
public class SingleVsMultipleDispatch {
static class Printer {
public void print(String str) {
System.out.println("Print string: " + str);
}
public void print(Integer num) {
System.out.println("Print integer: " + num);
}
}
public static void main(String[] args) {
Printer printer = new Printer();
Object obj1 = "Hello";
Object obj2 = 42;
// Java是单分派语言,这里根据参数静态类型决定
// printer.print(obj1); // 编译错误!
// printer.print(obj2); // 编译错误!
// 必须显式转换
printer.print((String) obj1); // 输出: Print string: Hello
printer.print((Integer) obj2); // 输出: Print integer: 42
}
}
更严格地说,Java采取的策略是静态多分派,动态单分派。
在编译过程中,Java首先会根据方法的接受者(或者说调用对象)的类型来确定方法本身,再根据方法的参数来确定方法是哪个重写的版本,这里有两个总量。
但是,在JVM运行其间,Java还会根据方法的接受者的实际类型,来确定方法调用的版本。
访问者模式
这里参考《秒懂设计模式》中提到的例子。当然,我对于这种设计模式的理解也不够深入,我仅仅想展现其中和分派相关的内容。
scala
public class V1 extends IV{
@Override
void accept(P1 p1) {
System.out.println("V1 accept P1");
}
@Override
void accept(P2 p2) {
System.out.println("V1 accept P2");
}
@Override
void accept(P3 p3) {
System.out.println("V1 accept P3");
}
}
假设这里存在一个Visitor的实现类,它会根据Pruduct的类型选择自己调用的方法。
ini
public static void main(String[] args) {
IP p1 = new P1();
IP p2 = new P2();
IP p3 = new P3();
IV v1 = new V1();
List<IP> list = Arrays.asList(p1, p2, p3);
for (IP ip : list) {
v1.accept( ip);
}
}
但是,这样写是失败的。因为这里是静态解析,也就是多分派的过程。首先看前面的类型,Visitor不存在接受IP这个类的方法,所以解析失败。虽然我们知道,上面3个Product的实际类型是满足要求的。
如何解决这个问题呢?
这里我们就把静态分派改成动态分派。我们不是让Visitor直接调用accept,而是让Product的实现类调用accept。
csharp
public abstract class IP {
abstract void accept(P1 p1);
}
接着我们写它的实现类
scala
public class P3 extends IP{
@Override
void accept(IV iv) {
iv.visit( this);
}
}
接着,回到client端,调用的时候,就可以直接:
scss
for (IP ip : list) {
ip.accept(v1);
}
为什么这里就可以使用了呢?
因为product存在accept这个方法,在解析的过程中能够找到,参数也匹配,所以成功多分派。
这里关注这一行
bash
89 invokevirtual #15 <com/gitee/visitor/IP.accept : (Lcom/gitee/visitor/IV;)V>
成功调用了一个虚方法。
当实际运行的时候,还会进行一个动态的单分派,根据方法的调用者的实际类型,选择调用的方法版本。
说完这些,我们来回答下一个问题,这里访问者模式究竟有什么用?
从JVM层面,它相当于绕开了静态分派的限制,在静态分派的代码里面直接调用需要动态分派的代码。
不过,对于一些非静态语言,比如说Python,这种迂回就是多余的,Python本来就是鸭子语言,等到运行时再报错是日常。
在效果层面,这里相当于,我们强制让product向下转型,visitor可以直接访问到真实的product类型,避免了我们手动操作的繁琐。
因为我经验太少,所以我也不太能够想到,到底什么场合需要这样做。
在《秒懂设计方法》一书中,作者使用的场景是购物车,购物车是抽象的商品列表,购物车里面有很多种不同的商品,消费者要根据商品的类型来调用不同的重写方法,使用访问者模式就避免了转型的繁琐。
JVM分派调用详细过程
1. 方法调用字节码指令
typescript
public class InvokeInstructions {
public void demonstrateInstructions() {
// invokestatic: 静态方法调用
Math.max(1, 2);
// invokevirtual: 实例方法调用(动态分派)
String str = "hello";
str.length();
// invokeinterface: 接口方法调用
List<String> list = new ArrayList<>();
list.size();
// invokespecial: 特殊方法调用(构造器、私有方法、父类方法)
Object obj = new Object();
}
}
2. 虚方法表(vtable)机制
scala
// JVM内部虚方法表结构
public class VirtualMethodTable {
class ClassMetadata {
String className;
ClassMetadata superClass;
Method[] vtable; // 虚方法表
public Method resolveVirtualMethod(int vtableIndex) {
return vtable[vtableIndex];
}
}
// 虚方法表示例
class AnimalVTable {
// Animal类的方法表
Method[] animalTable = {
Method("Animal.speak", Animal_speak_impl),
Method("Animal.eat", Animal_eat_impl),
// ...
};
}
class DogVTable extends AnimalVTable {
// Dog类的方法表(继承并重写)
Method[] dogTable = {
Method("Dog.speak", Dog_speak_impl), // 重写的方法
Method("Animal.eat", Animal_eat_impl), // 继承的方法
// ...
};
}
}
3. 动态分派的运行时解析
ini
public class DynamicDispatchRuntime {
public void invokevirtual(JavaThread thread, ConstantPool cp, int index) {
// 1. 从常量池解析方法符号引用
Method method = resolveMethod(cp, index);
// 2. 获取接收者对象(栈顶)
Object receiver = thread.getStack().peek();
// 3. 获取接收者的实际类型
Class<?> actualClass = receiver.getClass();
// 4. 在接收者类的方法表中查找目标方法
Method target = actualClass.vtable.lookup(method);
// 5. 如果找不到,在继承链中查找
if (target == null) {
target = findMethodInHierarchy(actualClass, method);
}
// 6. 执行目标方法
executeMethod(target, receiver);
}
private Method findMethodInHierarchy(Class<?> startClass, Method method) {
Class<?> current = startClass;
while (current != null) {
Method found = current.vtable.lookup(method);
if (found != null) return found;
current = current.getSuperclass();
}
throw new AbstractMethodError();
}
}
4. 接口方法分派
接口方法表(itable)
csharp
public class InterfaceDispatch {
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}
static class Duck implements Flyable, Swimmable {
public void fly() { System.out.println("Duck flying"); }
public void swim() { System.out.println("Duck swimming"); }
}
public static void main(String[] args) {
Flyable flyer = new Duck();
Swimmable swimmer = new Duck();
flyer.fly(); // 接口方法调用
swimmer.swim(); // 接口方法调用
}
}
接口方法解析过程
ini
public class InterfaceMethodResolution {
public void invokeinterface(JavaThread thread, ConstantPool cp, int index) {
// 1. 解析接口和方法
Interface iface = resolveInterface(cp, index);
Method method = resolveInterfaceMethod(cp, index);
// 2. 获取接收者对象
Object receiver = thread.getStack().peek();
Class<?> actualClass = receiver.getClass();
// 3. 在接收者类的接口方法表中查找
Method target = actualClass.itable.lookup(iface, method);
// 4. 如果找不到,搜索所有实现的接口
if (target == null) {
target = searchAllInterfaces(actualClass, iface, method);
}
// 5. 执行目标方法
executeMethod(target, receiver);
}
}
5. 分派优化技术
内联缓存(Inline Cache)
java
public class InlineCache {
// 内联缓存结构
class InlineCacheEntry {
Class<?> cachedClass; // 上次调用的类型
Method cachedMethod; // 对应的目标方法
int hitCount; // 命中次数
}
public Method lookupWithInlineCache(Object receiver, Method method) {
InlineCacheEntry cache = getCache(method);
// 检查缓存是否命中
if (cache.cachedClass == receiver.getClass()) {
cache.hitCount++;
return cache.cachedMethod;
}
// 缓存未命中,执行完整查找并更新缓存
Method target = performFullLookup(receiver, method);
updateCache(cache, receiver.getClass(), target);
return target;
}
}
多态内联缓存
arduino
public class PolymorphicInlineCache {
class PolymorphicCacheEntry {
Class<?>[] cachedClasses; // 多个缓存类型
Method[] cachedMethods; // 对应的多个方法
int size;
public Method lookup(Class<?> targetClass) {
for (int i = 0; i < size; i++) {
if (cachedClasses[i] == targetClass) {
return cachedMethods[i];
}
}
return null; // 缓存未命中
}
public void add(Class<?> clazz, Method method) {
if (size < cachedClasses.length) {
cachedClasses[size] = clazz;
cachedMethods[size] = method;
size++;
} else {
// 缓存满,退化为单态或超态
convertToMonomorphicOrMegamorphic();
}
}
}
}
6. 完整分派调用流程图
scss
方法调用开始
↓
检查字节码指令类型
↓
┌─────────────┐
│ invokestatic │ → 静态分派:直接调用
│ invokespecial │ → 静态分派:直接调用
└─────────────┘
↓
┌─────────────┐
│ invokevirtual │ → 动态分派开始
│ invokeinterface │
└─────────────┘
↓
获取接收者对象的实际类型
↓
检查内联缓存是否命中
↓
命中? → 执行缓存的方法
↓ 否
查找虚方法表(vtable)
↓
找到目标方法
↓
更新内联缓存
↓
执行目标方法
↓
方法调用结束
7. 性能影响和最佳实践
分派性能对比
java
public class DispatchPerformance {
private static final int ITERATIONS = 1000000000;
// 静态分派:最快
public static int staticMethod(int a, int b) {
return a + b;
}
// 单态调用:较快(内联缓存命中)
public int monomorphicCall(Animal animal) {
return animal.speak().length();
}
// 多态调用:较慢(需要查表)
public int polymorphicCall(Animal[] animals) {
int total = 0;
for (Animal animal : animals) {
total += animal.speak().length(); // 每次循环可能不同类型
}
return total;
}
// 巨态调用:最慢(缓存失效)
public int megamorphicCall(List<Animal> animals) {
int total = 0;
for (Animal animal : animals) {
total += animal.speak().length(); // 太多不同类型
}
return total;
}
}
优化建议
scss
public class DispatchOptimization {
// 不好的做法:过度多态
public void processAnimalsBad(List<Animal> animals) {
for (Animal animal : animals) {
animal.speak(); // 可能触发巨态分派
animal.eat();
animal.sleep();
}
}
// 好的做法:减少分派开销
public void processAnimalsGood(List<Animal> animals) {
for (Animal animal : animals) {
// 使用具体类型的方法,避免接口分派
if (animal instanceof Dog) {
processDog((Dog) animal);
} else if (animal instanceof Cat) {
processCat((Cat) animal);
}
}
}
private void processDog(Dog dog) {
dog.bark(); // 直接调用,无分派开销
dog.eat();
}
private void processCat(Cat cat) {
cat.meow(); // 直接调用,无分派开销
cat.eat();
}
}
总结
JVM方法分派调用核心要点:
-
静态分派:编译期确定,基于声明类型(方法重载)
-
动态分派:运行期确定,基于实际类型(方法重写)
-
分派机制:
- 虚方法表(vtable)用于类方法分派
- 接口方法表(itable)用于接口方法分派
- 内联缓存优化频繁调用的方法
-
性能考虑:
- 单态调用性能最佳
- 多态调用有查表开销
- 巨态调用性能最差
理解分派机制对于编写高性能Java代码和深入理解多态性至关重要。