【Spring】IoCDI详解

news2024/11/13 8:14:33

1. IoC详解

前面提到过IoC就是将对象的控制权交由Spring的IoC容器进行管理,由Spring的IoC容器创建和销毁bean,那么既然涉及到容器,就一定包含以下两方面功能:

  1. bean的存储
  2. bean的获取

1.1 类注解

Spring框架为了更好地服务应用程序,提供了非常丰富的注解用于存储bean,大致可以分为如下两类:

  1. 类注解:例如@Controller@Service@Repository@Configuration@Component
  2. 方法注解:例如:@Bean

接下来我们分别来看各类注解的使用:

1.1.1 @Controller(控制器存储)

1.1.1.1 bean的存储

使用@Controller存储bean的测试代码如下:

@Controller // 将bean存储到Spring容器中
public class TestController {
    public void testController() {
        System.out.println("test controller");
    }
}

那么我们如何才能够获取到Spring容器中的bean呢?

1.1.1.2 **bean的获取 **

我们可以在使用@SpringBootApplication的启动类中获取到ApplicationContext对象,该上下文对象就保存着程序启动时的一些信息,可以看做是IoC容器实例

@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取bean
        TestController controllerBean = context.getBean(TestController.class);
        // 3. 测试bean的testController方法
        controllerBean.testController();
    }
}

此时运行结果如下:
image.png
如果再把@Controller注解去掉,观察结果:
image.png
此时就会抛出这样一个异常:No qualifying bean of type 'com.example.ioc_di_demo.controller.TestController' available表明没有找到类型为TestController的bean实例

1.1.1.3 获取bean对象的其他方式

上述方式我们使用了通过类型的方式获取Bean,但是如果Spring容器中有多个同一类型的bean呢,我们应该如何进行获取?ApplicationContext也提供了其他的方式获取bean,这些方式都是其父接口BeanFactory提供的
image.png
其中第一种、第二种、第四种方式是比较常用的!其中1、2两种方式都涉及到name即根据名称来获取,那么bean的名称是什么呢?实质上,Spring容器在管理bean的时候会为每一个bean分配不同的唯一的名称来标识该bean实例,bean的命令规则如下(可观察源码java.beans.Introspector的decapitalize方法求证):

一般来说,如果程序员没有手动指定bean的名称,那么就会自动按照类名首字母小写,小驼峰形式,例如UserController => userController、AccountService => accountService

但是也会有特殊情况,当有多个字符,并且第一个和第二个字符都是大写字符是就保留原始的类名称

例如IService的bean名称就是IService

示例代码

@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2.1 获取bean(通过类型)
        TestController controllerBean1 = context.getBean(TestController.class);
        // 2.2 获取bean(通过名称)
        TestController controllerBean2 = (TestController) context.getBean("testController");
        // 2.3 获取bean(通过名称 + 类型)
        TestController controllerBean3 = context.getBean("testController", TestController.class);
        // 3.1 测试controllerBean1的testController方法
        controllerBean1.testController();
        // 3.2 测试controllerBean2的testController方法
        controllerBean2.testController();
        // 3.3 测试controllerBean3的testController方法
        controllerBean3.testController();
    }
}

1.1.2 @Service(业务逻辑存储)

1.1.2.1 bean的存储

我们也可以使用@Service注解将bean交由Spring容器进行管理

@Service // 将TestService作为bean交由Spring容器管理
public class TestService {
    public void testService() {
        System.out.println("test service");
    }
}
1.1.2.2 bean的获取

示例代码如下:

@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取TestService的bean对象
        TestService beanService = (TestService) context.getBean("testService");
        // 3. 调用bean对象的testService方法
        beanService.testService();
    }
}

运行结果
image.png

1.1.3 @Repository(仓库存储)

1.1.3.1 bean的存储
@Repository // 将bean存储到Spring容器
public class TestMapper {
    public void testRepository() {
        System.out.println("test repository");
    }
}
1.1.3.2 bean的获取
@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取TestMapper的bean对象
        TestMapper beanRepository = (TestMapper) context.getBean("testMapper");
        // 3. 调用bean对象的testRepository方法
        beanRepository.testRepository();
    }
}

1.1.4 @Component(组件存储)

1.1.4.1 bean的存储
@Component
public class TestComponent {
    public void testComponent() {
        System.out.println("test component");
    }
}
1.1.4.2 bean的获取
@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取TestComponent的bean对象
        TestComponent beanComponent = (TestComponent) context.getBean("testComponent");
        // 3. 调用bean对象的testComponent方法
        beanComponent.testComponent();
    }
}

1.1.5 @Configuration(配置存储)

1.1.5.1 存储bean
@Configuration
public class TestConfiguration {
    public void testConfiguration() {
        System.out.println("test configuration");
    }
}
1.1.5.2 获取bean
@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取TestConfiguration的bean对象
        TestConfiguration beanConfiguration = (TestConfiguration) context.getBean("testConfiguration");
        // 3. 调用bean对象的testConfiguration方法
        beanConfiguration.testConfiguration();
    }
}

1.1.6 为什么要使用那么多注解?

Spring为我们提供了各式各样的注解,其实与三层架构这样的应用分层思想不谋而合,可以让程序员方便的看到类的注解就明白该类的含义与作用:

  • @Controller:用户标识控制层(表现层),用于接收请求,返回处理完毕的响应结果
  • @Service:用于标识业务逻辑层,用于处理具体的业务逻辑
  • @Repository:用于标识数据层(持久层),负责数据访问操作即与数据库交互
  • @Configuration:用于标识配置层,管理程序中一些配置信息

各个注解之间的关系
观察五大注解的详细信息如下:
image.pngimage.png
image.pngimage.png
可以发现他们都是@Component的衍生注解,而@Component是一个元注解,可以被其他注解所使用

常见面试题:五大注解是否可以相互替换?

  1. 从功能上来说,大部分注解可以相互替换,但是唯独@Controller注解不可以使用别的注解替换,因为该注解用于表现层进行路由映射,使用其他注解代替有极小概率发生意想不到的错误
  2. 从语义性来说,不可以相互替换,@Controller用于标识控制层,@Service注解用于标识业务逻辑层,@Repository用于标识这是一个持久层

1.2 方法注解

上述我们介绍了五大类注解的使用,但是如果面对以下情况就无法使用类注解:

  1. 使用第JDK原生类/第三方包中的类,无法在类上使用注解
  2. 一个类需要多个对象,比如需要多个数据源对象

1.2.1 方法注解的使用

1.2.1.1 bean的配置
public class BeanConfig {

    @Bean
    public LocalDateTime localDateTime() {
        return LocalDateTime.now();
    }
}
1.2.1.2 bean的获取

上述代码我们使用@Bean方法注解配置了一个对象,交由Spring容器进行管理,下面我们尝试获取该实例

@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取LocalDateTime的bean对象
        LocalDateTime bean = context.getBean(LocalDateTime.class);
        // 3. 打印获取到的bean
        System.out.println(bean.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

image.png
但是此时却抛出了异常!这是因为 方法注解需要配合类注解使用!

1.2.1.3 方法注解需要配合类注解使用

将上述配置bean代码更改如下:

@Configuration
public class BeanConfig {

    @Bean
    public LocalDateTime localDateTime() {
        return LocalDateTime.now();
    }
}

此时程序正常运行:
image.png

1.2.1.4 定义多个对象

比如在多数据源的场景下,我们需要使用多个@Bean注解,如果依旧按照之前通过类型的方式获取,会出现什么问题呢?

@Configuration
public class BeanConfig {

    @Bean
    public LocalDateTime localDateTime() {
        return LocalDateTime.now();
    }

    @Bean LocalDateTime localDateTime2() {
        return LocalDateTime.of(2024, 4, 1, 12, 0, 0);
    }

    @Bean LocalDateTime localDateTime3() {
        return LocalDateTime.of(2024, 5, 1, 8, 0, 0);
    }
}

image.png
此时出现该异常,表示如果按照类型匹配,期望只得到一个实例,但是却匹配到了三个!这个时候我们就需要使用名称来获取bean了!

使用@Bean方法注解声明的bean的名称默认就是方法名称!

@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取TestConfiguration的bean对象
        LocalDateTime bean = (LocalDateTime) context.getBean("localDateTime");
        LocalDateTime bean2 = (LocalDateTime) context.getBean("localDateTime2");
        LocalDateTime bean3 = (LocalDateTime) context.getBean("localDateTime3");
        // 3. 打印获取到的bean
        System.out.println(bean.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(bean2.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(bean3.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

此时就可以正确获取结果!
image.png

1.3 配置扫描路径

Q:假设我们按照规则正确地使用注解配置bean,那么一定会被Spring容器进行管理吗?
A:不一定,我们还需要配置bean的扫描路径
image.pngimage.png
我们修改启动类的路径,再次运行程序!
image.png
此时程序运行错误了!其实原因就在于启动类中包含了注解@ComponentScan默认会扫描启动类所在的包及其子包,只有在指定路径下的bean才会被Spring容器管理,解决方式就是使用注解@ComponentScan配置扫描路径!

@ComponentScan(basePackages = {"com.example.ioc_di_demo"})
@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取TestConfiguration的bean对象
        LocalDateTime bean = (LocalDateTime) context.getBean("localDateTime");
        LocalDateTime bean2 = (LocalDateTime) context.getBean("localDateTime2");
        LocalDateTime bean3 = (LocalDateTime) context.getBean("localDateTime3");
        // 3. 打印获取到的bean
        System.out.println(bean.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(bean2.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        System.out.println(bean3.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
    }
}

2. DI详解

下面我们就来详细介绍下DI(Dependency Injection),中文翻译就是依赖注入,它是一种设计模式,用来实现IoC编程思想,它的核心思想就是将对象之间的依赖关系进行解耦合。通过依赖注入,可以为类的属性、构造方法、setter方法中提供所依赖的对象和值,是一种可重用、低耦合的思想
关于依赖注入,Spring提供了三种方式:

  1. 属性注入(Field Injection)
  2. 构造方法注入(Constructor Injection)
  3. setter注入(Setter Injection)

2.1 属性注入

Controller层代码如下:

@Controller // 将bean存储到Spring容器中
public class TestController {

    @Autowired
    private TestService testService; // 属性注入

    public void testDI() {
        // 调用属性方法
        testService.testService();
    }
}

此时在启动类中获取bean实例并调用testDI方法

@SpringBootApplication
public class IocDiDemoApplication {

    public static void main(String[] args) {
        // 1. 获取IoC容器
        ApplicationContext context = SpringApplication.run(IocDiDemoApplication.class, args);
        // 2. 获取TestController的bean对象
        TestController bean = (TestController) context.getBean("testController");
        // 3. 调用testDI方法
        bean.testDI();
    }
}

image.png
此时程序正常运行!说明Spring容器通过@Autowired注解已经自动使用 属性注入 的方式给TestService进行赋值了

2.2 构造方法注入

2.2.1 构造注入示例

此时如果我们不使用属性注入的方式,而是使用构造方法进行注入!代码示例如下:

@Controller // 将bean存储到Spring容器中
public class TestController {
    private TestService testService;

    public TestController(TestService testService) {
        this.testService = testService;
    }

    public void testDI() {
        // 调用属性方法
        testService.testService();
    }


    public void testController() {
        System.out.println("test controller");
    }
}

再次执行启动类代码,发现程序正常运行!

2.2.2 多个构造方法

此时我们将代码进行修改,提供多个构造方法:

@Controller // 将bean存储到Spring容器中
public class TestController {

    private TestService testService;

    private TestMapper testMapper;

    public TestController(TestService testService) {
        this.testService = testService;
    }

    public TestController(TestService testService, TestMapper testMapper) {
        this.testService = testService;
    }

    public void testDI() {
        // 调用属性方法
        testService.testService();
    }
}

image.png
此时就会发现程序报错了!

这是因为如果提供了多个构造器,那么Spring容器无法确定使用哪个构造器,因此默认会注入到无参构造器中,如果想要自己指定构造器,可以在构造方法上加上@Autowired注解

解决方式如下:

@Controller // 将bean存储到Spring容器中
public class TestController {
    
    private TestService testService;

    private TestMapper testMapper;

    @Autowired
    public TestController(TestService testService) {
        this.testService = testService;
    }

    public TestController(TestService testService, TestMapper testMapper) {
        this.testService = testService;
    }

    public void testDI() {
        // 调用属性方法
        testService.testService();
    }
}

2.3 setter注入

我们还可以提供setter方法注入类所需要的依赖对象

@Controller // 将bean存储到Spring容器中
public class TestController {

    private TestService testService;

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }

    public void testDI() {
        // 调用属性方法
        testService.testService();
    }
}

启动程序后,正常打印结果,说明setter方法同样可以进行依赖注入

2.4 三种注入方式优缺点分析

  • 属性注入:
    • 优点:比较简单,使用方便
    • 缺点:只能在IoC容器中进行使用;有可能出现NPE(空指针异常);而且不能注入Final修饰的属性
  • 构造注入:
    • 优点:可以注入Final修饰的属性;注入的对象不会被改变较为安全;依赖对象在使用之前一定会被完全初始化;通用性好(是JDK支持的)
    • 缺点:如果需要注入多个对象,代码比较冗长
  • setter注入:
    • 优点:方便在类实例创建完后,重新赋值或者注入
    • 缺点:不能注入Final修饰的属性;对象有可能被外部类访问调用,不安全

2.5 @Autowired存在的问题

下面来演示@Autowired注解存在的问题!
Config/BeanConfig.java

@Configuration
public class BeanConfig {

    @Bean
    public LocalDateTime localDateTime() {
        return LocalDateTime.now();
    }

    @Bean
    LocalDateTime localDateTime2() {
        return LocalDateTime.of(2024, 4, 1, 12, 0, 0);
    }

    @Bean
    LocalDateTime localDateTime3() {
        return LocalDateTime.of(2024, 5, 1, 8, 0, 0);
    }
}

我们将三个相同类型的LocalDateTime对象交由Spring容器进行管理!此时在TestConrtoller中尝试使用@Autowired注解就会发现编译失败!
image.png
这是因为@Autowired注解按照bean的类型进行自动装配,此时Spring容器内部有三个同一类型的bean,因此无法进行注入,解决方式有如下三种:

  1. 使用@Primary注解,告知Spring容器在获取该类型bean时优先选择该对象

image.png

  1. 使用@Autowired + @Qualifier搭配使用,配置value属性为bean的名称

image.png

  1. 使用@Resource注解声明bean的名称

image.png

常见面试题:@Autowired和@Resource注解的区别

  • @Autowired是Spring框架提供的注解,而@Resource是JDK提供的注解
  • @Autowired默认按照类型进行依赖注入,而@Resource是按照名称进行注入,相比较于@Autowired,@Resource提供了更多的注解参数设置

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RabbitMQ3.x之二_RabbitMQ所有端口说明及开启后台管理功能

RabbitMQ3.x之二_RabbitMQ所有端口说明及开启后台管理功能 文章目录 RabbitMQ3.x之二_RabbitMQ所有端口说明及开启后台管理功能1. RabbitMQ端口说明2. 开启Rabbitmq后台管理功能1. 查看rabbitmq已安装的插件2. 开启rabbitmq后台管理平台插件3. 开启插件后,再次查看插…

学习笔记Day15:Shell脚本编程

Shell脚本编程 Linux系统环境 Linux系统的4个主要部分:内核、shell、文件系统和应用程序。 内核是操作系统的核心,决定系统性能和稳定性shell :一种应用程序,是用户和内核交互操作的接口,是套在内核外的壳&#xff…

Linux文件IO(2):使用标准IO进行文件的打开、关闭、读写、流定位等相关操作

目录 前言 文件的打开和关闭的概念 文件的打开 文件的打开函数 文件打开的模式 文件的关闭 文件的关闭函数 注意事项 字符的输入(读单个字符) 字符输入的函数 注意事项 字符的输出(写单个字符) 字符输出的函数 注意…

实战|使用 Node.js 和 htmx 构建全栈应用程序

在本教程中,我将演示如何使用 Node 作为后端和 htmx 作为前端来构建功能齐全的 CRUD 应用程序。这将演示 htmx 如何集成到全栈应用程序中,使您能够评估其有效性并确定它是否是您未来项目的不错选择。 htmx 是一个现代 JavaScript 库,旨在通过…

微服务day07 -- 搜索引擎 ( 数据聚合 + 自动补全 + 数据同步 + ES集群 )

1.数据聚合 聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如: 什么品牌的手机最受欢迎? 这些手机的平均价格、最高价格、最低价格? 这些手机每月的销售情况如何? 实现这些…

ydata_profiling:自动生成数据探索报告的Python库

之前在做数据分析的时候,用过一个自动化生成数据探索报告的Python库:ydata_profiling 一般我们在做数据处理前会进行数据探索,包括看统计分布、可视化图表、数据质量情况等,这个过程会消耗很多时间,可能需要上百行代码…

数据库实验(一)SQL Server触发器

目录 触发器的定义 触发器和存储过程的区别 触发器的优点 触发器的作用 触发器的分类 DML触发器 DDL触发器 登录触发器 触发器的工作原理 inserted表 deleted表 创建触发器 编程要求 测试要求: 实验代码: 触发器的定义 触发器是建立在触…

视频推拉流EasyDSS点播平台云端录像播放异常的问题排查与解决

视频推拉流EasyDSS视频直播点播平台可提供一站式的视频转码、点播、直播、视频推拉流、播放H.265视频等服务,搭配RTMP高清摄像头使用,可将无人机设备的实时流推送到平台上,实现无人机视频推流直播、巡检等应用。 有用户反馈,项目现…

深度学习基础知识

本文内容来自https://zhuanlan.zhihu.com/p/106763782 此文章是用于学习上述链接,方便自己理解的笔记 1.深度学习的网络结构 深度学习是神经网络的一种特殊形式,典型的神经网络如下图所示。 神经元:表示输入、中间数值、输出数值点。例如&…

itextPdf生成pdf简单示例

文章环境 jdk1.8&#xff0c;springboot2.6.13 POM依赖 <dependency><groupId>com.itextpdf</groupId><artifactId>itextpdf</artifactId><version>5.5.13</version></dependency><dependency><groupId>com.ite…

【前端面试3+1】01闭包、跨域

一、对闭包的理解 定义&#xff1a; 闭包是指在一个函数内部定义的函数&#xff0c;并且该内部函数可以访问外部函数的变量。闭包使得函数内部的变量在函数执行完后仍然可以被访问和操作。 特点&#xff1a; 闭包可以访问外部函数的变量&#xff0c;即使外部函数已经执行完毕。…

【编译tingsboard】出现gradle-maven-plugin:1.0.11:invoke (default)

出现的错误&#xff1a; [ERROR] Failed to execute goal org.thingsboard:gradle-maven-plugin:1.0.11:invoke (default) on project http: Execution default of goal org.thingsboard:gradle-maven-plugin:1.0.11:invoke failed: Plugin org.thingsboard:gradle-maven-plugi…

输出当前时间

用途&#xff1a;在项目中一些属性中设置当前时间 实例代码 import java.time.LocalDateTime; import java.time.format.DateTimeFormatter;public class time {public static void main(String[] args){LocalDateTime china LocalDateTime.now(); DateTimeFormatter forma…

SpringBoot学习之ElasticSearch下载安装和启动(Mac版)(三十一)

本篇是接上一篇Windows版本,需要Windows版本的请看上一篇,这里我们继续把Elasticsearch简称为ES,以下都是这样。 一、下载 登录Elasticsearch官网,地址是:Download Elasticsearch | Elastic 进入以后,网页会自动识别系统给你提示Mac版本的下载链接按钮 二、安装 下载…

深度学习 - PyTorch基本流程 (代码)

直接上代码 import torch import matplotlib.pyplot as plt from torch import nn# 创建data print("**** Create Data ****") weight 0.3 bias 0.9 X torch.arange(0,1,0.01).unsqueeze(dim 1) y weight * X bias print(f"Number of X samples: {len(…

huawei 华为 交换机 配置 LACP 模式的链路聚合示例 (交换机之间直连)

组网需求 如 图 3-22 所示&#xff0c; SwitchA 和 SwitchB 通过以太链路分别都连接 VLAN10 和 VLAN20 的网络&#xff0c;且SwitchA 和 SwitchB 之间有较大的数据流量。用户希望 SwitchA 和 SwitchB 之间能够提供较大的链路带宽来使相同VLAN 间互相通信。在两台 Switch 设备上…

flask_restful规范返回值之参数设置

设置重命名属性和默认值 使用 attribute 配置这种映射 , 比如&#xff1a; fields.String(attributeusername) 使用 default 指定默认值&#xff0c;比如&#xff1a; fields.String(defaultsxt) from flask import Flask,render_template from flask_restful import A…

C++ 输入与输出

数据输入输出 数据是程序处理的对象&#xff0c;输入输出操作是程序中不可或缺少的部分&#xff0c;C中输入输出操作都是通过函数调用实现的&#xff1b;C提供了一个“标准I/O库”。 格式输出函数-----printf 基本形式是&#xff1a; printf(“控制字符串”,输出项列表); 功能…

openwrt在校园网环境下开启nat6 (ipv6 nat)

如果将路由器接入校园网&#xff0c;我们只能获得一个128位掩码的ipv6地址。这个地址仅供路由器本身使用&#xff0c;而路由器后的设备无法获取到ipv6地址&#xff0c;因此我们可以利用网络地址转换&#xff08;NAT&#xff09;为这些设备分配本地ipv6地址。 下面是openwrt开启…

【C++练级之路】【Lv.16】红黑树(冰与火的碰撞,红与黑的史诗)

快乐的流畅&#xff1a;个人主页 个人专栏&#xff1a;《C语言》《数据结构世界》《进击的C》 远方有一堆篝火&#xff0c;在为久候之人燃烧&#xff01; 文章目录 引言一、红黑树的概念二、红黑树的模拟实现2.1 结点2.2 成员变量2.3 插入情况一&#xff1a;uncle在左&#xff…