Spring依赖注入思想分析

news2024/11/26 22:25:43

Spring 依赖注入思想分析

文章目录

  • `Spring` 依赖注入思想分析
    • 一、前言
    • 二、控制反转(`Inversion of Control`)
      • 1. 代码依赖初始化问题
      • 2. 匿名内部类解决方案
      • 3. 创建接口实现类方案
      • 4. 问题深入
      • 5. 定义父类解决问题1方案
      • 6. 控制反转解决问题2方案
    • 三、依赖注入(`Dependency Injection`)
      • 1、依赖关系树
      • 2、 依赖注入框架
      • 3、 Spring的依赖注入
      • 4、 ApplicationContext
    • 参考文章

背景

​ 大二期间一直很疑惑,为什么 Spring 要把创建对象弄的这么复杂?

​ 从各种对象的创建,装配,到销毁,什么事情都是 Spring 给干了,感觉给我带来的无非就是少了几个 new 对象的语句,以及使用 @Autowired 把对象注入了就能用,没感觉太过于方便在哪里。

​ 大三了之后,就慢慢开始对 依赖注入 这种思想有了一点点似是而非的认识,突然觉得,这么做好像真的还挺好!

一、前言

​ 在维基百科中,依赖注入(dependency injection)简称DI,定义如下:

define:在软件工程中,依赖注入dependency injection)的意思为,给予调用方它所需要的事物。“依赖”是指可被方法调用的事物。依赖注入形式下,调用方不再直接指使用“依赖”,取而代之是“注入” 。“注入”是指将“依赖”传递给调用方的过程。在“注入”之后,调用方才会调用该“依赖”。传递依赖给调用方,而不是让让调用方直接获得依赖,这个是该设计的根本需求。在编程语言角度下,“调用方”为对象和类,“依赖”为变量。在提供服务的角度下,“调用方”为客户端,“依赖”为服务。

​ 看起来是不是有点阅读困难?

​ 在我们理解它在编程之中的含义之前,我们来聊聊什么是依赖依赖是指依靠某种东西来获得支持,比如我会说我们对手机的依赖程度过高。那么如果我们将依赖,放入在编程的世界中,又是什么表现形式呢?

example:当 class A 使用 class B 的某些功能的时候,则表示 class A 具有 class B的依赖,在Java 中,使用其他class的方法前,我们需要创建对应的 class 的对象。(即 class A 需要创建一个 class B 的实例)

结论因此,将创建对象的任务转移给其他 class,并直接使用依赖项的过程,被称为“依赖项注入”。

​ 了解依赖注入之后,本文再来聊聊 Spring 框架中DISpring Inversion of Control(控制反转),ApplicationContext (应用上下文)的故事。

二、控制反转(Inversion of Control

Tips:控制反转(IOC)是一种编程思想,它改变了传统程序的执行流程。想象一下,传统程序像是你亲自开车,而IOC则像是坐公交。在IOC中,你不再亲自控制每个步骤,而是把控制权交给框架公交车,框架会帮你处理很多琐碎的事情,你只需专注于自己的目的地业务逻辑。比如Spring框架帮你管理对象、处理依赖关系,使代码更简洁、灵活。

作者:阿妮亚学SqlSugar
链接:https://www.zhihu.com/question/381216328/answer/3366856030

​ 让我们简单来了解学习一下控制反转Java 代码中是如何体现这一思想的,我会尽可能讲述的简单,请各位细细体会每一次我改变的意图,和我们的解决方案,这个对于理解什么是控制反转的思想很重要。

1. 代码依赖初始化问题

​ 通常来说,我们实例化一个对象的方式一般是使用 new 这个关键字来进行实例化一个对象。如下代码展示

Car car = new Car();

​ 这样,我们就实例化了一辆车,显然,这个车是有一定基本属性的,以及这辆车我们也可以将其分割成好几大类,车由轮胎,引擎,车身,电气设备等等组成。在这里为了类尽可能简单,我们定义 Engine 接口来模拟汽车引擎,然后将 engine 对象作为变量成员放入 Car 类中,这样,我们构建了一个依赖关系 CarEngine 组成。

定义接口 Engine ,因为引擎可能有多个不同种类的引擎,但是只要符合有 trunOn() 方法的引擎,我们就都可以拿来用。

public interface Engine{
    void turnOn();
}

定义类 class Car

package org.example.car;

/**
 * @ClassName Car
 * @Description TODO
 * @Author 枫飘长安
 * @Date 2024/3/30 11:42
 * @Version 1.0
 **/
public class Car {
    
    private Engine engine;

    public Car() {
    }

    public void start() {
        engine.turnOn();
    }

}

显然,这个时候当我们调用 car.start() 方法时,会抛出NullPointerException空指针异常,为啥呢?因为你连引擎都没有啊,你引擎怎么启动?

这个时候你可能会好奇,为什么啊,在 Car 中的 start() 方法中,不是有 engine.turnOn(); 吗?但是你可能忽略了一个实时,你没有在 Car 的构造函数中初始化 engine,也就是说,此时你的 enginenull

package org.example;

import org.example.car.Car;

public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        car.start();
    }
}

2. 匿名内部类解决方案

当然,你可以使用匿名内部类的方式进行如下改进,在匿名内部类中重写turnOn() 方法,并成功初始化 engine

package org.example.car;

/**
 * @ClassName Car
 * @Description TODO
 * @Author 枫飘长安
 * @Date 2024/3/30 11:42
 * @Version 1.0
 **/
public class Car {
    private Engine engine;

    public Car() {
    }

    public void start() {
        engine = new Engine() {
            @Override
            public void turnOn() {
                System.out.println("Engine started");
            }
        };
        engine.turnOn();
    }

}

但是在工程中,我们往往不会这样去实现,以下,是使用匿名内部类的优劣。在一个庞大的工程体系中,匿名内部类往往是不太明智的选择。

匿名内部类优劣性

优势:

  1. 简洁性:如果只是简单地需要一个具有特定行为的Engine实例,并且只需要在一个地方使用,匿名内部类能很好地满足需求,无需单独创建一个新的类文件
  2. 灵活性:这种方式允许你在使用时即时定义和覆盖父类或接口的方法,根据当前方法的需求提供定制化的行为。

劣势:

  1. 可读性和可维护性:如果这个匿名内部类实现的功能较为复杂,或者有多个方法需要重写,代码量就会变得很大,降低代码的可读性和可维护性。
  2. 复用性:如果同样的逻辑需要在多处使用,那么每次都需要重新定义匿名内部类,不如直接创建一个独立的、命名的类来得方便和高效。
  3. 测试难度:匿名内部类由于其匿名特性,不容易被单独提取出来进行单元测试。

3. 创建接口实现类方案

其次,我们可以创建新的类,来实现 Engine 接口

ElectricEngine

public class ElectricEngine implements Engine {
    @Override
    public void turnOn(){
        System.out.println("电动引擎启动");
    }
}

CombustionEngine

public class CombustionEngine implements Engine {
    @Override
    public void turnOn(){
        System.out.println("燃油引擎启动");
    }
}

好,此时我们修改 Car 的构造函数,比如说我用 ElectricEngine 实现,将我们的 engine 字段分配给一个实例化的 ElectricEngine对象

public class Car {
    private Engine engine;
    
    public Car(){
        this.engine = new ElectricEngine();
    }
    
    public void start(){
        engine.turnOn();
    }
}

完成上述操作后,启动我们的 Main 函数,这个时候控制台打印信息,我们也达成了我们的目的。

电动引擎启动

创建接口实现类优劣性

优势:

  1. 清晰的职责划分:接口定义了一组行为规范接口实现类负责具体的实现细节,符合面向对象设计原则中的单一职责原则接口隔离原则,有助于提高代码的可读性和可维护性。

  2. 高内聚低耦合:不同的类可以实现同一个接口,可以根据实际需求灵活替换不同的实现类,增强了系统的扩展性和解耦程度

  3. 易于测试:可以通过Mock框架轻松模拟接口实现类的行为,便于单元测试集成测试

  4. 代码复用:接口实现类可以在多个地方被引用和复用,避免代码重复编写

劣势:

  1. 额外的类结构:相比于匿名内部类,创建接口及其实现类需要更多的类结构和代码组织,可能会增加项目的复杂度。
  2. 过度设计风险:如果不恰当的设计过多接口,可能会导致系统过于复杂,尤其是在需求不明确或变化频繁的情况下,可能导致接口难以适应后续需求变更。
  3. 运行时类型转换:在使用多态特性时,有时需要进行类型判断或类型转换,增加了程序的复杂性。

4. 问题深入

反思:我们解决了空指针异常,但是,我们胜利了吗?这就是完美的银弹了吗?

问题延伸:在解决问题的同时,我们又引入了另一个问题。尽管我们通过抽象Engine接口,然后通过不同的Engine实现类来负责不同类型引擎的业务逻辑,的确是很好的设计策略。但是细心的伙伴可能已经发现了,我们Car类的构造函数中将engine声明为ElectricEngine,这将导致所有车都有一个电动引擎。假如我们现在要创建不同的汽车对象它有一个燃油引擎我们将不得不改变我们的设计。比较常见的方法是创建两个独立里的类,各司其职,在他们的构造函数中将engine分配给Engine接口的不同实现;

例如:

创建 CombustionCar 燃油车类

public class CombustionCar {

    private Engine engine;

    public CombustionCar() {
        this.engine = new CombustionEngine();
    }

    public void start() {
        engine.turnOn();
    }

}

创建 ElectricCar 电动车类

public class ElectricCar {
    private Engine engine;

    public ElectricCar() {
        this.engine = new ElectricEngine();
    }

    public void start() {
        engine.turnOn();
    }

}

通过上述一顿操作,我们大大增加了我们的代码量,以后更是幸福,因为每多一种引擎,都得重新创建一个对应该引擎的车类,我们程序猿实在是太幸福了!

如果只是日常需求,我们已经可以不用动以上的代码了,因为这已经足够使用了,但是这种解决方案显然不是我写这篇文章的目的。

Tips:从设计角度来说,当前代码是糟糕的,有两个问题始终没有得到解决。

  1. 在两个不同的类中,都有重复的 start() 方法,代码冗余。
  2. 我们需要为每一个新的 Engine 实现类创建一个新的类。

随着整个工程的庞大化,第二个问题会越来越严重,成为**“屎山”**的一部分。

5. 定义父类解决问题1方案

带上上述两个问题继续思考,先聚焦到问题一,代码冗余问题。

这个时候Java 三大特性之一的继承知识就可以用上了!我们可以创建一个父类 Car,把公共代码抽取到父类中,就解决了第一个问题。

由于 Engine 字段是私有的,我们在父类 Car 的构造函数中接受一下 Engine 对象,并进行赋值即可。

Car

public class Car {

    private Engine engine;

    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.turnOn();
    }
}

CombustionCar

public class CombustionCar extends Car{

    public CombustionCar() {
        super(new CombustionEngine());
    }

}

ElectricCar

public class ElectricCar extends Car {

    public ElectricCar() {
        super(new ElectricEngine());
    }

}

通过使用父类并继承的方式,我们成功解决了代码重复的问题,测试一下我们的 Main 函数

package org.example;

import org.example.engine.CombustionCar;
import org.example.engine.ElectricCar;

public class Main {
    public static void main(String[] args) {
        CombustionCar combustionCar = new CombustionCar();
        combustionCar.start();
        ElectricCar electricCar = new ElectricCar();
        electricCar.start();
    }
}

能够得到输出

电动引擎启动
燃油引擎启动

此时,我们应该有的项目结构如下

在这里插入图片描述

6. 控制反转解决问题2方案

​ 现在,我们换一个角度看待问题2,为什么我们需要去关注 CombustionCarElectricCar 呢?这些不都是 Car 吗?于是,我们把关注点重新回到我们的 Car 上面来。

​ 我们现在已经允许在实例化 Car 对象的时候,将 Engine 对象作为构造函数的参数传入,其实这样已经消除了为每个 Engine 对象创建新的 Car 子类的问题。父类 Car依赖于 Engine 接口,并不需要知道任何关于 Engine的实现。

​ 通过带有Engine参数的构造函数,我们已将要使用哪个Engine实现的决定从Car类本身(最初由CombustionEngine决定)更改为实例化Car类的客户端。决策过程的这种逆转称为IoC原则。现在,由客户端控制使用哪种实现,而不是由Car类本身控制使用哪种Engine实现。

Tips:这个思想理解有点绕,大家可以结合示例代码进行理解

    public static void main(String[] args) {

        /**
         * 老法子
         * 为每一类型发送机的车创建类,然后实现父类car,然后在构造函数传入自己的引擎,然后调用start()
         */
        CombustionCar combustionCar = new CombustionCar();
        combustionCar.start();
        ElectricCar electricCar = new ElectricCar();
        electricCar.start();

        /**
         * 控制反转思想
         * 把自己看作实例化car的客户端,需要什么引擎,直接传入相关对象
         */
        CombustionEngine combustionEngine = new CombustionEngine();
        Car combustionCar = new Car(combustionEngine);
        combustionCar.start();
        ElectricEngine electricEngine = new ElectricEngine();
        Car electricCar = new Car(electricEngine);
        electricCar.start();
    }

​ 从上面的例子我们可以看到,实例化 Car 类的客户端可以控制所使用的Engine实现,并且取决于将哪个Engine实现传递给Car构造函数,Car对象的行为发生巨大变化。接下来就是依赖注入的领域了,两者结合,妙不可言。

三、依赖注入(Dependency Injection

Tips:在上面控制反转的知识点,解决了由谁决定使用哪种Engine实现的问题,但是不可避免,我们也更改了实例化一个Car对象的步骤;

Step-1:最开始,我们实例化Car不需要参数,因为在它的构造函数里面已经为我们newEngine对象。

Step-2:使用IoC方法之后,我们要求在实例化一个Car之前,我们需要先创建一个Engine对象,并作为参数传递给Car构造对象。

区别:

  1. 最初,我们首先实例化Car对象,然后实例化Engine对象。但是,使用IoC之后,我们首先实例化Engine对象,然后实例化Car对象;
  2. 因此,我们在上面的过程中创建了一个依赖关系。
  3. 这种依赖关系不是指编译时候 Car类对Engine接口的依赖关系,相反,我们构建了一个运行时依赖关系
  4. 运行时,实例化Car对象之前,必须首先实例化Engine对象。

思考:我们构建的这种运行时依赖关系会给我们带来什么样的影响呢?

1、依赖关系树

某一个具体的依赖对象大家可以理解为Spring中的bean,对于两个有依赖关系的bean,其中被依赖的那个bean,我们把它称为依赖对象

​ 我们用图形化的方式来看看它们之间的依赖关系,其中图形的节点代表对象,箭头代表依赖关系(箭头指向依赖对象)。对于我们我的Car类,依赖关系树非常简单:

在这里插入图片描述

​ 如果依赖关系树的终端结点还有自己的附加依赖关系,那么这个依赖关系树将变得更加复杂。

​ 现在再看我们上面的例子,如果 CombustionEngine 还有其他依赖对象,我们首先需要创建CombustionEngine依赖对象,然后才能实例化一个CombustionEngine对象。这样在创建Car对象时候,才能将CombustionEngine传递给Car的构造函数;

//凸轮轴        
public class Camshaft {}
//机轴
public class Crankshaft {}

public class CombustionEngine implements Engine {

  //凸轮轴
  private Camshaft camshaft;

  //机轴
  private Crankshaft crankshaft;

  public CombustionEngine(Camshaft camshaft, Crankshaft crankshaft) {

      this.camshaft = camshaft;

      this.crankshaft = crankshaft;
  }

  @Override

  public void turnOn() {

      System.out.println("燃油引擎启动");

  }

}

经过我们改造,我们现在的依赖关系树变为下面的样子

在这里插入图片描述

2、 依赖注入框架

​ 随着我们不断引入更多的依赖关系,这种复杂性将继续增长。为了解决这个复杂问题,我们需要基于依赖关系树抽取对象的创建过程。这就是依赖注入框架

一般来说,我们可以把这个过程分为三个部分:

  1. 声明需要创建的对象需要哪些依赖对象
  2. 注册创建这些依赖对象所需要的类
  3. 提供一种使用1和2两点思想创建对象的机制

​ 通过反射,我们可以查看 Car 类的构造函数,并且知道它需要一个 Engine 参数。因此为了创建Car对象,我们必须创建至少一个Engine接口的实现类用作依赖项来使用。在这里,我们创建一个CombustionEngine 对象(为了方便,暂时当做只有一个实现类,bean冲突问题待会再说)来声明它作为依赖项来使用,就满足Car对象创建时的需求。

​ 其实,这个过程是递归的,因为CombustionEngine 依赖于其他对象,我们需要不断重复第一个过程,直到把所有依赖对象声明完毕,然后注册创建这些依赖对象所需要的类。

​ 第三点其实就是将前面两点思想付诸实施,从而形成一种创建对象的机制

举个例子

  1. 比如我们需要一个Car对象,我们必须遍历依赖关系树并检查是否存在至少一个符合条件的类来满足所有依赖关系。

  2. 例如,声明CombustionEngine类可满足Engine节点要求。如果存在这种依赖关系,我们将实例化该依赖关系,然后移至下一个节点。

  3. 如果有一个以上的类满足所需的依赖关系,那么我们必须显式声明应该选择哪一种依赖关系。

  4. 一旦我们确定所有的依赖关系都准备好了,我们就可以从终端节点开始创建依赖对象。

  5. 对于 Car 对象,我们首先实例化 CamshaftCrankshaftーー因为这些对象没有依赖关系ーー然后将这些对象传递给 CombustionEngine 构造函数,以实例化 CombunstionEngine 对象。最后,我们将 CombunstionEngine 对象传递给 Car 构造函数,以实例化所需的 Car 对象。

了解了 DI 的基本原理之后,我们现在可以继续讨论 Spring 如何执行 DI

3、 Spring的依赖注入

TipsSpring的核心是一个DI框架,它可以将DI配置转换为Java应用程序。

​ 在这里我们要阐述一个问题:那就是库和框架的区别库只是类定义的集合

​ 背后的原因仅仅是代码重用,即获取其他开发人员已经编写的代码。这些类和方法通常在域特定区域中定义特定操作。例如,有一些数学库可让开发人员仅调用函数而无需重做算法工作原理的实现。

框架通常被认为是一个骨架,我们在其中插入代码以创建应用程序。许多框架保留了特定于应用程序的部分,并要求我们开发人员提供适合框架的代码。在实践中,这意味着编写接口的实现,然后在框架中注册实现。

在这里插入图片描述

4、 ApplicationContext

​ 在 Spring 中,框架围绕 ApplicationContext 接口实现上一节中概述的三个 DI 职责。通常这个接口代表了一个上下文。因此,我们通过基于 java 或基于 xml 的配置向 ApplicationContext 注册合适的类,并从 ApplicationContext 请求创建 bean 对象。然ApplicationContext 构建一个依赖关系树并遍历它以创建所需的 bean对象。

在这里插入图片描述

Applicationcontext 中包含的逻辑通常被称为 Spring 容器。通常,一个 Spring 应用程序可以有多个 ApplicationContext,每个 ApplicationContext 可以有单独的配置。例如,一个 ApplicationContext 可能被配置为使用 CombustionEngine 作为其引擎实现,而另一个容器可能被配置为使用 ElectricEngine 作为其实现。

参考文章

  1. 为什么我们需要依赖注入? - 知乎 (zhihu.com)
  2. 据说,80%的人没有真正理解了Spring的依赖注入 - 知乎 (zhihu.com)
  3. 依赖注入是什么?如何使用它? (freecodecamp.org)

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

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

相关文章

Swift:“逻辑运算子“与“比较运算符“

1. 逻辑非 ! 逻辑非运算符 ! 是用于对布尔值取反的。当操作数为 true 时,! 将返回 false,而当操作数为 false 时,! 将返回 true。 let isTrue true let isFalse !isTrue // isFalse 现在是 false 2. 逻辑与 && 逻辑与运算符 &a…

CPU 密集型 和 IO密集型 的区别,如何确定线程池大小?

CPU密集型任务 是指需要大量的CPU资源进行计算的任务 CPU密集型任务通常涉及到复杂的计算,如算法逻辑、数学计算等,其特点是CPU使用率高,多在这种类型的任务中,线程数量一般与CPU的核心数相匹配就足够了。 一个简单的估算方法是…

线性CCD

线性CCD 综述:本文讲述了线性CCD是什么、由什么组成、工作原理、芯片TSL401的引脚和时序、线性CCD的时序。 1. 定义 线性CCD,只能采集一行像素,分辨率为128,也即是线性CCD≈128个光电传感器。经过光照时,光电二极管…

微信小程序开发【从入门到精通】——页面事件

👨‍💻个人主页:开发者-曼亿点 👨‍💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨‍💻 本文由 曼亿点 原创 👨‍💻 收录于专栏&#xff1a…

三个表的联合查询的场景分析-场景4:c表维护a和b表的id关联关系(一对多)

基础SQL演练,带详细分析,笔记和备忘。 目录 背景介绍 表数据 需求1:查询g表所有记录,以及关联的h的id 需求2:在需求1基础上,查出关联的h的其它字段(name) 需求3:在需…

ssm009毕业生就业信息统计系统+vue

毕业生就业信息统计系统 摘 要 随着移动应用技术的发展,越来越多的学生借助于移动手机、电脑完成生活中的事务,许多的行业也更加重视与互联网的结合,以提高快捷、高效、安全,可以帮助更多有需求的人。针对传统毕业生就业信息统计…

瑞吉外卖实战学习--6、通过try和catch进行异常处理

try和catch进行异常处理 效果图前言1、公共拦截器进行异常处理1.1、创建公共报错处理的方法1.2、@ControllerAdvice中设置要拦截的类1.3、@ExceptionHandler中写处理的异常类2、完善错误拦截器2.1、效果效果图 前言 当用户名重复数据库会报错,此时就需要捕获异常操作 1、公共…

【文末 附 gpt4.0升级秘笈】超越Sora极限,120秒超长AI视频模型诞生

120秒超长AI视频模型发布:开启视频生成新纪元 随着人工智能技术的迅猛发展,AI视频生成领域也取得了令人瞩目的突破。近日,一项名为“StreamingT2V”的120秒超长AI视频模型正式发布,标志着文生视频技术正式进入长视频时代。这一技…

Spring官方真的不建议使用属性进行依赖注入吗?

使用Spring进行依赖注入时,很多大佬都推荐使用构造方法注入,而非使用在属性上添加 Autowired 注入,而且还说这是Spring官方说的,真的是这样吗? 使用Spring进行依赖主要的方式有很多,主流的使用方式有两种&a…

kaggle竞赛(房价预测)(Pytorch 06)

一 下载数据集 此数据集由Bart de Cock于2011年收集,涵盖了2006‐2010年期间 亚利桑那州 埃姆斯市的房价。 下载地址: import hashlib import os import tarfile import zipfile import requests#save DATA_HUB dict() DATA_URL http://d2l-data.s3…

linux在使用重定向写入文件时(使用标准C库函数时)使处理信号异常(延时)--问题分析

linux在使用重定向写入文件时(使用标准C库函数时)使处理信号异常(延时)–问题分析 在使用alarm函数进行序号处理测试的时候发现如果把输出重定向到文件里面会导致信号的处理出现严重的延迟(ubuntu18) #include <stdio.h> #include <stdlib.h> #include <unist…

Java23种常见设计模式汇总

七大原则网站地址&#xff1a;设计模式7大原则&#xff0b;类图关系-CSDN博客 创建型设计模式&#xff1a;创建型设计模式合集-CSDN博客 七大结构型设计模式&#xff1a;7大结构型设计模式-CSDN博客 11种行为型设计模式&#xff1a; 11种行为型模式&#xff08;上&#xff0…

LeetCode---390周赛

题目列表 3090. 每个字符最多出现两次的最长子字符串 3091. 执行操作使数据元素之和大于等于 K 3092. 最高频率的 ID 3093. 最长公共后缀查询 一、每个字符最多出现两次的最长子字符串 非常经典的滑动窗口问题&#xff0c;即动态维护一段区间&#xff0c;使得这段区间满足…

数据结构:Trie(前缀树/字典树)

文章目录 一、介绍Trie1.1、Trie的结点结构1.2、Trie的整体结构 二、Trie的操作2.1、Trie插入操作2.2、Trie查找操作2.3、Trie前缀匹配操作2.4、Trie删除操作 三、实战3.1、实现Trie&#xff08;前缀树&#xff09; 一、介绍Trie Trie 又称字典树、前缀树和单词查找树&#xff…

短视频素材那里来?五大平台让你的视频大放异彩!

哈喽&#xff01;短视频创作者们&#xff0c;是不是在寻找那些能让你的剪辑视频更加闪耀的素材&#xff1f;别着急&#xff0c;今天我要为你们带来五个超棒的视频素材网站&#xff0c;让你的作品在抖音、快手上大放异彩&#xff0c;成为众人羡慕的焦点&#xff01; 蛙学网&…

python电商结合双轨制

最近又重新整合翻看以前的数据&#xff0c;图片&#xff0c;绘画&#xff0c;还有各种编程代码&#xff0c;python,leetcode,还有关于商业方面的一些见解,想起了大学时候和同学们并肩作战&#xff0c;熬夜编码的时光。还有大数据&#xff0c;八爪鱼爬虫。 下面是我的手稿电商打…

输出单链表倒数第K个结点值

方法一&#xff1a; 两次遍历链表。第一次遍历&#xff0c;计算链表长度&#xff0c;然后计算链表倒数第m个结点的正数位置k&#xff0c;判断位置是否合法&#xff0c;如果不合法&#xff0c;输出NOT FOUND&#xff0c;否则&#xff0c;进行第二次遍历链表&#xff0c;查找链表…

【初阶数据结构】——牛客:CM11 链表分割

文章目录 1. 题目介绍2. 思路分析3. 代码实现 1. 题目介绍 链接: link 这道题是给我们一个链表和一个值X &#xff0c;要求我们以给定值x为基准将链表分割成两部分&#xff0c;所有小于x的结点排在大于或等于x的结点之前。 最终返回重新排列之后的链表的头指针。 2. 思路分析…

根据实例逐行分析NIO到底在做什么

Selector&#xff08;选择器&#xff09;是 Channel 的多路复用器&#xff0c;它可以同时监控多个 Channel 的 IO 状况&#xff0c;允许单个线程来操作多个 Channel。Channel在从Buffer中获取数据。 选择器、通道、缓冲池是NIO的核心组件。 一、新建选择器 此时选择器内只包含…

Python学习笔记-简单案例实现多进程与多线程

Python 的多进程与多线程是并发编程的两种重要方式&#xff0c;用于提高程序的执行效率。它们各自有不同的特点和适用场景。 多进程&#xff08;Multiprocessing&#xff09; 概念&#xff1a; 多进程是指操作系统中同时运行多个程序实例&#xff0c;每个实例称为一个进程。…