先看一个“反直觉” 的代码示例
看下面这个例子
public class Fruit {
public void printSelf(){
}
}
public class Apple extends Fruit {
}
public class Pear extends Fruit {
}
public class NamePrinter {
//参数是Fruit,打印Fruit
public void print(Fruit fruit){
System.out.println("print fruit");
}
//参数是Apple,打印Apple
public void print(Apple apple){
System.out.println("print apple");
}
//参数是Pear,打印Pear
public void print(Pear pear){
System.out.println("print pear");
}
}
@Test
public void test1(){
Fruit fruit = new Fruit();
var apple = new Apple();
var pear = new Pear();
var namePrinter = new NamePrinter();
namePrinter.print(fruit);
namePrinter.print(apple);
namePrinter.print(pear);
}
很简单的例子, 相信大家都不会出错, 输出
print fruit
print apple
print pear
让我们稍微修改一下代码,只改test方法里面变量的申明,其余不变
@Test
public void test1(){
Fruit fruit = new Fruit();//var 修改为 Fruit
Fruit apple = new Apple();//var 修改为 Fruit
Fruit pear = new Pear();//var 修改为 Fruit
NamePrinter namePrinter = new NamePrinter();
namePrinter.print(fruit);
namePrinter.print(apple);
namePrinter.print(pear);
}
想想, 输出是啥?
👇
print fruit
print fruit
print fruit
惊不惊喜?为什么仅修改变量的声明类型,方法调用行为就完全不同?这背后涉及 Java 的分派机制。
分派
定义: 分派是指 JVM 在调用方法时,根据对象或参数的类型选择具体实现的过程。Java 的分派机制分为两个维度:时机(编译时 / 运行时)和维度(一维 / 多维)。
精简一点: 分派:类型决定方法执行
- 静态分派:发生在编译阶段,依据变量的声明类型(静态类型)。
- 动态分派:发生在运行阶段,依据对象的实际类型(动态类型)。
静态分派
NamePrinter的三个print方法, 具有相同的名称,但是参数类型不一样,属于方法重载。
public void print(Fruit person){
System.out.println("print fruit");
}
public void print(Apple apple){
System.out.println("print apple");
}
public void print(Pear pear){
System.out.println("print pear");
}
在编译时,编译器只知道apple, pear 是Fruit类型,因此运行时调用重载方法的时候, 只能调用参数为申明Fruit类型的方法。
Fruit fruit = new Fruit();
Fruit apple = new Apple();
Fruit pear = new Pear();
NamePrinter namePrinter = new NamePrinter();
namePrinter.print(fruit);
namePrinter.print(apple);
namePrinter.print(pear);
public void print(Fruit fruit){
System.out.println("print fruit");
}
静态分派决定调用哪个重载方法。
动态分派
怎么修改?
第一种方法是使用var 来申明变量, 这种方式其实是利用java 语法糖, 生成出来的.class文件其实是在编译时就修改成了申明的类型和new 后面对应的类型一致,实际上还是编译时来解决问题
第二种方式是通过运行时来动态根据对象类型来选择调用方法。
最简单直接的,用instanceof 来解决
public class NamePrinter {
public void print(Fruit fruit) {
if(fruit instanceof Apple){
System.out.println("print apple");
}else if(fruit instanceof Pear){
System.out.println("print pear");
}else{
System.out.println("print fruit");
}
}
}
进阶一点的, 每个子类去重写print方法.
public class Fruit {
public void print() {
System.out.println("print fruit");
}
}
public class Apple extends Fruit {
//字类重写print方法
public void print() {
System.out.println("print apple");
}
}
public class Pear extends Fruit {
//字类重写print方法
public void print() {
System.out.println("print pear");
}
}
public class NamePrinter {
public void print(Fruit fruit) {
fruit.print();
}
}
public class TestFruit {
@Test
public void test1() {
Fruit fruit = new Fruit();
Fruit apple = new Apple();
Fruit pear = new Pear();
NamePrinter namePrinter = new NamePrinter();
namePrinter.print(fruit);
namePrinter.print(apple);
namePrinter.print(pear);
}
}
输出
print fruit
print apple
print pear
单分派 vs 双分派:分派机制的维度升级
单分派
上面第二种优化方案,其实就是单分派。 NamePrinter只有一个类, NamePrinter.print(Fruit fruit)根据Fruit的参数, 来决定调用Fruit的哪个子类的print方法。 这种单一维度的类型信息来决定调用哪个方法,属于单分派。
双分派
但在某些复杂场景中,我们需要根据两个维度的类型信息来决定执行哪个方法,这就是双分派 。
比如NamePrinter也有多个字类呢? 怎么在运行时,根据NamePrinter和Fruit的对象类型,来动态的知道调用哪个NamePrinter的print方法,以及调用哪个Fruit字类的print方法呢?这就是双分派问题。
怎么解决双分派问题? JAVA 不支持双分派, 访问者模式是唯一的解决方法。
总结
Java 方法调用的 “反直觉” 表现源于分派机制
- 分派机制分为两个维度:时机,维度
- 按时机来说, 分派有静态分派(编译时) , 动态分派(运行时).
- 静态分派-重载
- 动态分派-重写
- 按维度来说, 有单分派, 双分派
- 单分派:仅一维类型判断(声明或实际类型)。
- 双分派:需二维类型协作(如元素类型 + 操作类型),Java 需通过访问者模式实现,通过两次动态调用解耦多维度逻辑。
核心结论:分派维度决定代码灵活性,双分派是复杂场景下的解耦关键,访问者模式是 Java 的标准解法。