重写、重载、访问者模式

Java是面向对象的编程语言,面向对象的三大特点是封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。

这里我们详细谈论一下多态的实现。

多态是指同一个方法调用可以在不同的对象上表现出不同的行为。多态分为编译时多态(方法重载)和运行时多态(方法重写)。

这里需要简单介绍一下方法调用:

方法调用不等于方法被执行,方法调用阶段的唯一任务是确定方法的版本。

在Java文件中,我们通过.调用方法。

scss 复制代码
 //Test类名
 //test实例名
 Test.staticMethod()//直接调用本类的静态方法可以省略类名
 test.method()//直接调用本类的实例方法可以省略this

但是,这只是代码而已,它怎么落诸实现呢?

首先,代码会被编译,但是,Java的编译过程不涉及连接步骤,编译好的Class文件中存在的仅仅是对于方法的符号引用,而不是方法在实际运行时的内存布局的入口地址,也就是直接引用。

方法的直接引用需要在类加载乃至运行时才能确定。

类加载时确定方法

这一类方法满足如下特点

  1. 编译器可知。
  2. 运行时不可变。

主要有静态方法和私有方法。静态方法和类绑定,私有方法外部不可访问。

私有方法这里还需要解释一下

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方法分派调用核心要点

  1. 静态分派:编译期确定,基于声明类型(方法重载)

  2. 动态分派:运行期确定,基于实际类型(方法重写)

  3. 分派机制

    • 虚方法表(vtable)用于类方法分派
    • 接口方法表(itable)用于接口方法分派
    • 内联缓存优化频繁调用的方法
  4. 性能考虑

    • 单态调用性能最佳
    • 多态调用有查表开销
    • 巨态调用性能最差

理解分派机制对于编写高性能Java代码和深入理解多态性至关重要。

相关推荐
不是株1 天前
JavaWeb(后端进阶)
java·开发语言·后端
编程火箭车1 天前
【Java SE 基础学习打卡】02 计算机硬件与软件
java·电脑选购·计算机基础·编程入门·计算机硬件·软件系统·编程学习路线
Felix_XXXXL1 天前
IDEA + Spring Boot 的三种热加载方案
java·后端
我命由我123451 天前
IDEA - IDEA 快速回到页面首尾、页面快速滑动、快速定位到指定行
java·运维·ide·后端·java-ee·intellij-idea·intellij idea
Mickyจุ๊บ1 天前
IDEA 插件推荐
java·ide·intellij-idea
命运之光1 天前
【快速解决】idea运行javafx错误: 缺少 JavaFX 运行时组件, 需要使用该组件来运行此应用程序
java·ide·intellij-idea
学到头秃的suhian1 天前
Maven
java·maven
小坏讲微服务1 天前
Docker-compose 搭建Maven私服部署
java·spring boot·后端·docker·微服务·容器·maven