重写、重载、访问者模式

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代码和深入理解多态性至关重要。

相关推荐
市场部需要一个软件开发岗位4 分钟前
JAVA开发常见安全问题:Cookie 中明文存储用户名、密码
android·java·安全
忆~遂愿8 分钟前
GE 引擎进阶:依赖图的原子性管理与异构算子协作调度
java·开发语言·人工智能
MZ_ZXD00112 分钟前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东15 分钟前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology20 分钟前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble24 分钟前
springboot的核心实现机制原理
java·spring boot·后端
人道领域32 分钟前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七1 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
CodeToGym1 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel
凡人叶枫2 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发