《异步编程之美》— 全栈修仙《Java 8 CompletableFuture 对比 ES6 Promise 以及Spring @Async》

   哈喽,大家好!在平常开发过程中会遇到许多意想不到的坑,本篇文章就记录在开发过程中遇到一些常见的问题,看了许多博主的异步编程,我只能说一言难尽。本文详细的讲解了异步编程之美,是不可多得的好文,建议细细品尝。文章末尾附有思维导图以及参考文档。

        首先,我们得弄明白什么异步?JavaScript 语言的执行环境是单线程的,异步编程对于 JavaScript 来说必不可少。JavaScript 传统异步解决方案主要是通过回调函数,而回调函数最大的问题就是 Callback Hell。所以 ES6 标准提供的 Promise 对象,专门用于解决异步编程的问题。而 Java 语言是一个支持多线程的语言,语法以同步为主,在实际开发中很少需要用到大量的异步编程。但是要想追求更高的性能,异步通常是更好的选择。例如 Servlet 3 的异步支持、Spring 5 提供的 Spring WebFlux 等,都是为了追求更高的性能。和 JavaScript 一样,传统的 Callback 方式处理 Java 异步也会有 Callback Hell 问题,所以在 Java 8 中新增了和 ES6 的 Promise 类似的对象: java.util.concurrent.CompletableFuture 。

        其次,类似与前端 Promise 代表 异步对象,类似Java中的 CompletableFuture。Promise 是现代 JavaScript 中异步编程的基础,是一个由异步函数返回的可以向我们指示当前操作所处的状态的对象。在 Promise 返回给调用者的时候,操作往往还没有完成,但 Promise 对象可以让我们操作最终完成时对其进行处理(无论成功还是失败)。

        最后,@Async是Spring提供的一个异步注解,默认采用SimpleAsyncTaskExecutor线程池,该线程池不是真正意义上的线程池。使用此线程池无法实现线程重用,每次调用都会新建一条线程。若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。所以我们得自定义线程池

public void execute(Runnable task, long startTimeout) {
  Assert.notNull(task, "Runnable must not be null");
  Runnable taskToUse = this.taskDecorator != null ? this.taskDecorator.decorate(task) : task;
  //判断是否开启限流,默认为否
  if (this.isThrottleActive() && startTimeout > 0L) {
    //执行前置操作,进行限流
    this.concurrencyThrottle.beforeAccess();
    this.doExecute(new SimpleAsyncTaskExecutor.ConcurrencyThrottlingRunnable(taskToUse));
  } else {
    //未限流的情况,执行线程任务
    this.doExecute(taskToUse);
  }

}

protected void doExecute(Runnable task) {
  //不断创建线程
  Thread thread = this.threadFactory != null ? this.threadFactory.newThread(task) : this.createThread(task);
  thread.start();
}

//创建线程
public Thread createThread(Runnable runnable) {
  //指定线程名,task-1,task-2...
  Thread thread = new Thread(this.getThreadGroup(), runnable, this.nextThreadName());
  thread.setPriority(this.getThreadPriority());
  thread.setDaemon(this.isDaemon());
  return thread;
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * 描述:异步配置2
 * 
 */
@Configuration
@EnableAsync    // 可放在启动类上或单独的配置类
public class AsyncConfiguration2 {
    @Bean(name = "asyncPoolTaskExecutor")
    public ThreadPoolTaskExecutor executor() {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        //核心线程数
        taskExecutor.setCorePoolSize(10);
        //线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        taskExecutor.setMaxPoolSize(100);
        //缓存队列
        taskExecutor.setQueueCapacity(50);
        //许的空闲时间,当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        taskExecutor.setKeepAliveSeconds(200);
        //异步方法内部线程名称
        taskExecutor.setThreadNamePrefix("async-");
        /**
         * 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略
         * 通常有以下四种策略:
         * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
         * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
         * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
         * ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功
         */
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        taskExecutor.initialize();
        return taskExecutor;
    }
}
//使用异步编程操作大量数据 

List<Long> collect = examSysQuestionReals.stream()
        .map(i -> CompletableFuture.supplyAsync(i::getId))
        .map(CompletableFuture::join)
        .collect(Collectors.toList());
        
 /** 与普通stream.map()对比
 优化后的代码中,stream.map() 分为两步:
第一次 map():将每个 examSysQuestionReal 对象转换为一个 CompletableFuture,该异步任务在后台线程(通常是线程池中的线程)中执行 i.getId() 方法。这意味着多个 getId() 操作可以同时进行,实现了并行处理。
第二次 map():对上一步产生的 CompletableFuture 列表调用 join() 方法,等待所有异步任务完成并获取它们的结果。这些结果随后被收集到一个新的 List<Long> 中。
区别总结:
执行模式:直接使用 stream.map() 时,映射操作是同步且顺序执行的;优化后的代码则利用 CompletableFuture 实现了异步并行执行。
性能:对于耗时的 getId() 操作(例如涉及数据库查询或其他远程服务调用),优化后的代码能利用多核处理器的能力,通过并行处理显著减少整体处理时间。而直接使用 stream.map() 只能在单一线程中顺序执行,无法利用多核优势,处理大量数据或耗时操作时可能效率较低。
资源消耗:优化后的代码引入了异步任务,可能增加线程池的使用和上下文切换的开销。然而,只要 getId() 操作的并行效益超过这些开销,总体上仍能提高性能。直接使用 stream.map() 无需额外的线程池资源,但在处理大规模或耗时任务时可能会因无法并行而显得低效。
综上所述,直接使用 stream.map() 适用于同步、轻量级且无需并行处理的场景。而优化后的代码结合 CompletableFuture 更适合处理可能耗时、受益于并行计算的任务,以提高程序的整体执行效率。在实际应用中,应根据具体需求和性能指标选择合适的方法
 /
 
//进阶,@Async与CompletableFuture
/**
在这个示例中,performComplexAsyncTask() 方法被标记为 @Async,由 Spring 异步执行。方法内部使用 CompletableFuture 实现了多个步骤的异步任务创建、组合和结果处理。这样既利用了 Spring 的异步方法抽象,又充分利用了 CompletableFuture 的灵活性和控制力。
总结来说,CompletableFuture 和 @Async 可以分别进行练习,然后在实践中结合使用,以适应不同复杂度和需求的异步编程场景。
/
@Service
public class AsyncService {

    @Async
    public CompletableFuture<String> performComplexAsyncTask(String input) {
        // Step 1: 异步获取数据
        CompletableFuture<String> dataFuture = CompletableFuture.supplyAsync(() -> {
            // 这里可能是耗时的数据库查询或网络请求
            return fetchData(input);
        });

        // Step 2: 异步处理数据,依赖于第一步的结果
        CompletableFuture<String> processedDataFuture = dataFuture.thenApply(this::processData);

        // Step 3: 异步发送通知,不依赖于前两步的结果,可以并发执行
        CompletableFuture<Void> notificationFuture = CompletableFuture.runAsync(() -> {
            sendNotification();
        });

        // Step 4: 等待所有异步操作完成
        return CompletableFuture.allOf(processedDataFuture, notificationFuture)
                .thenApply(unused -> {
                    // 返回最终处理结果或相关信息
                    return processedDataFuture.join();
                });
    }

    // 其他方法:fetchData(), processData(), sendNotification()
}

 /**
 在实际应用中,CompletableFuture 和 @Async 可以结合使用,发挥各自的优势。
使用 @Async 注解标记那些业务逻辑相对独立、适合作为单独任务执行的方法。这有助于简化代码结构,将异步处理逻辑从业务逻辑中分离出来。
在 @Async 方法内部,可以使用 CompletableFuture 构建更复杂的异步流程,如组合多个异步操作、处理中间结果等。这种方法结合了 Spring 的高级抽象与 CompletableFuture 的强大功能。
/  

现在我们从线程池配置类实战一次



import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 配置线程池
 * 
 */
@Configuration
@EnableAsync
public class ExecutorConfig {


	/* 获取快递单号线程池,量大 配置 start */
    // 维持最小的线程数
    private int corePoolSizeAutoTrace = 10;
    // 最大的活动线程数
    private int maxPoolSizeAutoTrace = 50;
    // 排队的线程数
    private int queueCapacityAutoTrace = maxPoolSizeAutoTrace * 2;
    // 线程长时间闲置关闭的时间,单位秒
    private int keepAliveSecondsAutoTrace = 1 * 60 * 60;

    private String asyncTask = "asyncTask-";
    private String eventExecute = "eventExecute-";

    private String pushStatus = "pushStatus-";

    // 异常记录
    private int recordCoreSize = 5;
    // 最大的活动线程数
    private int recordMaxPoolSize = 10;


    // 异常记录
    private int orderCoreSize = 10;
    // 最大的活动线程数
    private int orderMaxPoolSize = 100;

    /**
     * 异步任务线程池
     * @return 执行器
     */
    @Bean
    public Executor asyncTask() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSizeAutoTrace);
        executor.setMaxPoolSize(maxPoolSizeAutoTrace);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix(asyncTask);
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName(asyncTask);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Bean
    public Executor eventExecute() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSizeAutoTrace);
        executor.setMaxPoolSize(maxPoolSizeAutoTrace);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix(eventExecute);
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName(eventExecute);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Bean
    public Executor pushStatus() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSizeAutoTrace);
        executor.setMaxPoolSize(maxPoolSizeAutoTrace);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix(pushStatus);
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName(pushStatus);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    @Bean
    public Executor addOperationLogList() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSizeAutoTrace);
        executor.setMaxPoolSize(maxPoolSizeAutoTrace);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix("addOperationLog-");
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName("addOperationLog-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    /**
     * 新增托运单时,添加单据收发方信息
     * @return
     */
    @Bean
    public Executor addBillLogisticsInfo() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSizeAutoTrace);
        executor.setMaxPoolSize(maxPoolSizeAutoTrace);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix("addBillLogisticsInfo-");
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName("addBillLogisticsInfo-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    /**
     * 派车单异常记录
     */
    @Bean(name = "recordBoxExecutor")
    public Executor recordBoxExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(recordCoreSize);
        executor.setMaxPoolSize(recordMaxPoolSize);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix("addTmsBillRecordBoxDtl-");
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName("addTmsBillRecordBoxDtl-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    /**
     * 运单回写揽收线程池
     */
    @Bean(name = "orderHandExecutor")
    public Executor orderHandExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSizeAutoTrace);
        executor.setMaxPoolSize(maxPoolSizeAutoTrace);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix("orderHandExecutor-");
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName("orderHandExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

    /**
     * 运单回写签收线程池
     */
    @Bean(name = "orderSignExecutor")
    public Executor orderSignExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSizeAutoTrace);
        executor.setMaxPoolSize(maxPoolSizeAutoTrace);
        executor.setQueueCapacity(queueCapacityAutoTrace);
        executor.setThreadNamePrefix("orderSignExecutor-");
        executor.setKeepAliveSeconds(keepAliveSecondsAutoTrace);
        executor.setThreadGroupName("orderSignExecutor-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

在代码中使用

PS:线程安全问题及解决方法

参考文档:

网址:CompletableFuture 指南 |贝尔东 (baeldung.com)

思维图解:CompletableFuture的使用| ProcessOn免费在线作图,在线流程图,在线思维导图

优雅处理并发:Java CompletableFuture最佳实践 - 个人文章 - SegmentFault 思否

Java 8 CompletableFuture 对比 ES6 Promise | 叉叉哥的BLOG (xxgblog.com)

SpringBoot 实现异步调用@Async | 以及使用@Async注解可能会导致的问题_springboot @async异步类被aop拦截会报什么错误-CSDN博客

 线上调优:接口响应慢?那是你没用 CompletableFuture 来优化!

一次真实生产事故,让我总结了线程池的正确使用方式 (qq.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/953247.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

day07_Spark SQL

文章目录 day07_Spark SQL课程笔记一、今日课程内容二、Spark SQL函数定义&#xff08;掌握&#xff09;1、窗口函数2、自定义函数背景2.1 回顾函数分类标准:SQL最开始是_内置函数&自定义函数_两种 2.2 自定义函数背景 3、Spark原生自定义UDF函数3.1 自定义函数流程&#x…

presto不支持concat_ws

在sparksql/hive中&#xff0c;将一个数据集合已指定的分隔符拼接可以用concat_ws&#xff0c;但是在presto中没有这个函数&#xff0c;不过presto提供了一个集合方法array_join&#xff0c;来达到相同的目的 同样的对数据集去重可以用array_distinct 如果你不需要去重就直接…

【日常小记】Ubuntu启动后无图形界面且网络配置消失

【日常小记】Ubuntu启动后无图形界面且网络配置消失 解决方法&#xff1a; 1. 输入后恢复网络: #sudo dhclient 2. 重新安装ubuntu-desktop #sudo apt-get install ubuntu-desktop&#xff01;&#xff01;&#xff01;请关注是否能ping通某网站&#xff08;例如百度&…

01、kafka知识点综合

kafka是一个优秀大吞吐消息队列&#xff0c;下面我就从实用的角度来讲讲kafka中&#xff0c;“kafka为何有大吞吐的机制”&#xff0c;“数据不丢失问题”&#xff0c;“精准一次消费问题” 01、kafka的架构组织和运行原理 kafka集群各个节点的名称叫broker&#xff0c;因为kaf…

【ArcGIS微课1000例】0137:色彩映射表转为RGB全彩模式

本文讲述ArcGIS中,将tif格式的影像数据从色彩映射表转为RGB全彩模式。 参考阅读:【GlobalMapper精品教程】093:将tif影像色彩映射表(调色板)转为RGB全彩模式 文章目录 一、色彩映射表预览二、色彩映射表转为RGB全彩模式一、色彩映射表预览 加载配套数据包中的0137.rar中的…

Python教程丨Python环境搭建 (含IDE安装)——保姆级教程!

工欲善其事&#xff0c;必先利其器。 学习Python的第一步不要再加收藏夹了&#xff01;提高执行力&#xff0c;先给自己装好Python。 1. Python 下载 1.1. 下载安装包 既然要下载Python&#xff0c;我们直接进入python官网下载即可 Python 官网&#xff1a;Welcome to Pyt…

2025.1.13运算符重载和继承

作业 #include <iostream> #include <cstring> using namespace std; //在之前做的mystring类的基础上&#xff0c;将能够重载的运算符全部进行重载class mystring { private:char *str;int size;public://无参构造mystring():size(10){str new char[size];str[0…

慧集通(DataLinkX)iPaaS集成平台-业务建模之业务对象(二)

3.UI模板 当我们选择一条已经建好的业务对象点击功能按钮【UI模板】进入该业务对象的UI显示配置界面。 右边填写的是UI模板的编码以及对应名称&#xff1b;菜单界面配置以业务对象UI模板编码获取显示界面。 3.1【列表-按钮】 展示的对应业务对象界面的功能按钮配置&#xff1…

springboot使用Easy Excel导出列表数据为Excel

springboot使用Easy Excel导出列表数据为Excel Easy Excel官网&#xff1a;https://easyexcel.opensource.alibaba.com/docs/current/quickstart/write 主要记录一下引入时候的pom&#xff0c;直接引入会依赖冲突 解决方法&#xff1a; <!-- 引入Easy Excel的依赖 -->&l…

计算机的错误计算(二百一十)

摘要 利用两个大模型计算 . 若可能&#xff0c;保留10位有效数字。实验表明&#xff0c;一个大模型给出了错误结果。另外一个大模型提供了 Python代码&#xff1b;运行代码后&#xff0c;输出中有2位错误数字。 例1. 计算 . 若可能&#xff0c;保留10位有效数字。 下面是一…

用vscode+ollama自定义Cursor AI编辑的效果

在vscode上搜索Continue 添加大语言模型 选择对应的本地模型版本 效果

基于微信小程序的汽车销售系统的设计与实现springboot+论文源码调试讲解

第4章 系统设计 一个成功设计的系统在内容上必定是丰富的&#xff0c;在系统外观或系统功能上必定是对用户友好的。所以为了提升系统的价值&#xff0c;吸引更多的访问者访问系统&#xff0c;以及让来访用户可以花费更多时间停留在系统上&#xff0c;则表明该系统设计得比较专…

ANSYS Fluent学习笔记(七)求解器四部分

16.亚松弛因子 Controls面板里面设置&#xff0c;它能够稳定计算的过程。如果采用常规的迭代算法可能结果就会发生振荡的情况。采用亚松驰因子可以有助于残差的稳定。 他的取值范围是0-1&#xff0c;0代表没有亚松驰&#xff0c;1表示物理量变化很快&#xff0c;一般情况下取…

【MySQL数据库】基础总结

目录 前言 一、概述 二、 SQL 1. SQL通用语法 2. SQL分类 3. DDL 3.1 数据库操作 3.2 表操作 4. DML 5. DQL 5.1 基础查询 5.2 条件查询 5.3 聚合函数 5.4 分组查询 5.5 排序查询 5.6 分页查询 6. DCL 6.1 管理用户 6.2 权限控制 三、数据类型 1. 数值类…

【数学】概率论与数理统计(五)

文章目录 [toc] 二维随机向量及其分布随机向量离散型随机向量的概率分布律性质示例问题解答 连续型随机向量的概率密度函数随机向量的分布函数性质连续型随机向量均匀分布 边缘分布边缘概率分布律边缘概率密度函数二维正态分布示例问题解答 边缘分布函数 二维随机向量及其分布 …

mysql中创建计算字段

目录 1、计算字段 2、拼接字段 3、去除空格和使用别名 &#xff08;1&#xff09;去除空格 &#xff08;2&#xff09;使用别名&#xff1a;AS 4、执行算术计算 5、小结 博主用的是mysql8 DBMS&#xff0c;附上示例资料&#xff1a; 百度网盘链接: https://pan.baidu.co…

uniapp 之 uni-forms校验提示【提交的字段[‘xxx‘]在数据库中并不存在】解决方案

目录 场景问题代码结果问题剖析解决方案 场景 uni-forms官方组件地址 使用uniapp官方提供的组件&#xff0c;某个表单需求&#xff0c;单位性质字段如果是高校&#xff0c;那么工作单位则是高校的下拉选择格式&#xff0c;单位性质如果是其他的类型&#xff0c;工作单位则是手动…

【SH】Xiaomi9刷Windows10系统研发记录 、手机刷Windows系统教程、小米9重装win10系统

文章目录 参考资料云盘资料软硬件环境手机解锁刷机驱动绑定账号和设备解锁手机 Mindows工具箱安装工具箱和修复下载下载安卓和woa资源包第三方Recovery 一键安装Windows准备工作创建分区安装系统 效果展示Windows和Android一键互换Win切换安卓安卓切换Win 删除分区 参考资料 解…

苹果电脑怎么清理后台,提升苹果电脑运行速度

苹果电脑以其流畅的系统和高效的性能备受用户青睐&#xff0c;但即使是性能强大的Mac&#xff0c;随着使用时间的增长&#xff0c;也会遇到运行变慢、卡顿的问题。造成这种现象的一个主要原因是后台运行的程序和进程过多&#xff0c;占用了系统资源。那么&#xff0c;苹果电脑怎…

qt 快捷功能 快速生成 setter getter 构造函数 父类虚函数重写 成员函数实现 代码框架 查看父类及父类中的虚函数

qt 快速生成 setter getter 构造函数 父类虚函数重写 成员函数实现 代码框架 1、找到要实现的头文件 2、鼠标移动到在头文件中的类定义的类名上&#xff0c;右键进行选择。 这是插入父类虚函数(父类虚函数重写) 选项弹出来的结果。可以查看到所有父类及父类中的所有的虚函数