当前位置: 首页 > news >正文

设计模式二:代理模式

1、什么是动态代理

可能很多小伙伴首次接触动态代理这个名词的时候,或者是在面试过程中被问到动态代理的时候,不能很好的描述出来,动态代理到底是个什么高大上的技术。不方,其实动态代理的使用非常广泛,例如我们平常使用的 Spring中的 @Transactional注解,其依赖于 AOP,而 AOP的底层实现便是动态代理,看到这里,是不是更有兴趣去了解动态代理了呢?

动态代理:可以分解为"动态"+“代理”。

  • 代理:"代理"一词,在我们的生活中也是随处可见的,例如房屋中介,其是对房主的一种代理,房主需要出租其房屋,但是可能没时间去接待租客,给租客介绍房屋信息,带领租客看房,但是房屋中介可以为租客提供这些服务,所以,代理其是对被代理对象的一个功能增强
  • 动态:"动态"通常与"静态"相比较,"静态"描述的是事物是固定存在的,"动态"则描述的是事物是随着需求而动态生成的。

所以,静态代理存在一定的局限性,不能很好的满足需求的千变万化,动态代理的出现,就是为了解决这些局限性。

我们先来看看静态代理。

2.、静态代理

在开发中,通常需要为方法添加日志打印,能够记录程序的执行过程,以便后续出现异常问题的时候,能更好的排查定位。

假设我们现在已经完成了系统用户的增加、删除、修改等功能,这些功能在类 UserServiceImpl中已经实现。

代码示例:

public class UserServiceImpl {public void add() {System.out.println("添加用户");}public void update() {System.out.println("修改用户");}public void delete() {System.out.println("删除用户");}
}

现在,我们需要在UserServiceImpl类中的方法添加日志功能,那么怎么才能更好地去实现这个需求呢?

1)直接在目标方法前后添加日志代码

代码示例:

public class UserServiceImpl {public void add() {System.out.println("====== add方法开始 ======");System.out.println("添加用户");System.out.println("====== add方法结束 ======");}public void update() {System.out.println("====== update方法开始 ======");System.out.println("修改用户");System.out.println("====== update方法结束 ======");}public void delete() {System.out.println("====== delete方法开始 ======");System.out.println("删除用户");System.out.println("====== delete方法结束 ======");}
}

观察上述代码,这种方式的缺点在于:

2)静态代理方式实现

静态代理需要我们将目标类的方法抽取到接口中,代理类和目标类实现同一个接口,既然要实现代理,代理类自然需要在其内部维护目标对象的引用,并通过构造函数为其赋值,然后在代理类的方法中调用目标对象的同名方法,并在调用前后完成功能的增强。

实现步骤:

  • 抽取UserService接口
  • 创建目标类UserServiceImpl实现UserService接口
  • 创建代理类UserServiceProxy实现UserService接口
  • 代理类中完成功能的增强

代码实现:

// 目标接口
public interface UserService {void add();void update();void delete();
}
// 目标类
public class UserServiceImpl implements UserService {@Overridepublic void add() {System.out.println("添加用户");}@Overridepublic void update() {System.out.println("修改用户");}@Overridepublic void delete() {System.out.println("删除用户");}
}
// 代理类
public class UserServiceProxy implements UserService {private UserService userService;public UserServiceProxy(UserService userService) {this.userService = userService;}@Overridepublic void add() {System.out.println("====== add方法开始 ======");userService.add();System.out.println("====== add方法结束 ======");}@Overridepublic void update() {System.out.println("====== update方法开始 ======");userService.update();System.out.println("====== update方法结束 ======");}@Overridepublic void delete() {System.out.println("====== delete方法开始 ======");userService.delete();System.out.println("====== delete方法结束 ======");}
}

观察上述代码,静态代理遵循开闭原则,在不修改目标类的前提下,完成了功能的增强,但是依然存在大量重复的代码,且一个代理类只能代理一个目标类,如果有n个目标类需要被代理,就需要同比增加n个代理类。

那么,有没有办法可以使得我们不需要去定义这么多的代理类,就可以实现对目标类功能的增强?答案是有的:动态代理

3、JDK动态代理

前面,我们提到静态代理的实现方式:代理类和目标类都实现同一个接口,在代理类中维护目标类对象,并完成对目标类对象方法的增强,这种方式虽然遵循开闭原则,但是代理类和目标类至少是"一对一"的绑定关系,如果需要被代理的目标类个数越多,代理类就会越多,会产生大量重复的代码,也不利于后期的维护。

从静态代理中,我们知道代理类也是接口的一个实现类,代理对象的类型也是属于接口类型,我们来验证一下。

public class Test {public static void main(String[] args) {UserServiceProxy userServiceProxy = new UserServiceProxy(new UserServiceImpl());System.out.println(userServiceProxy instanceof UserService);}
}
// 打印结果:true

那么,能不能动态生成这些代理对象呢?我们知道类是构造对象的模板,代理类都还不存在,怎么去构造代理对象呢?

除了不存在代理类,还剩下 UserService接口和 UserServiceImpl目标类,JDK动态代理的目的就是通过接口来生成代理类以及代理类的对象,我们知道接口是不能直接通过new关键字创建对象的。

那么JDK动态代理是怎么创建出代理类以及代理类对象的呢?

我们先来看看通过 new关键字创建对象的过程。

UserServiceImpl userService = new UserServiceImpl();
/*
创建对象的过程:1.执行new指令,如果类未加载,先执行类加载过程。1.加载:JVM通过ClassLoader将UserServiceImpl.class文件加载到方法区(Method Area),在堆内存中创建代表该类的Class对象。2.验证3.准备:为静态变量分配内存并设置类型初始值。4.解析5.初始化:为静态变量赋值、执行静态代码块2.为对象分配内存,将对象的实例字段初始化类型零值。3.执行构造方法,对对象进行初始化
*/

追踪上述过程,我们得知创建对象,需要先得到该类的Class对象,通过Class对象去创建实例对象。为了验证这一点,我们不妨来看看通过反射的方式创建对象的过程。

public class Test {@SneakyThrowspublic static void main(String[] args) {// 获取Class对象Class<UserServiceImpl> userServiceClass = UserServiceImpl.class;// 获取构造器Constructor<?>[] constructors = userServiceClass.getConstructors();for (Constructor<?> constructor : constructors) {// 通过构造器创建实例对象System.out.println(constructor.newInstance());}}
}

现在,问题回归到接口不能直接new,也没有构造方法,并且不存在代理类的class文件,怎么获得Class对象了。

动态代理关键类

我们先来看看JDK动态代理的实战代码:

  • 需要自定义个 CustomInvocationHandler实现 InvocationHandler接口。
  • 利用 Proxy.newProxyInstance构建实例对象。
// UserService接口
public interface UserService {void add();void update();void delete();
}// 目标类
public class UserServiceImpl implements UserService {@Overridepublic void add() {System.out.println("添加用户");}@Overridepublic void update() {System.out.println("修改用户");}@Overridepublic void delete() {System.out.println("删除用户");}
}// CustomInvocationHandler
public class CustomInvocationHandler implements InvocationHandler {// 目标对象private Object target;public CustomInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("====== 方法开始 ======");Object result = method.invoke(target, args);System.out.println("====== 方法结束 ======");return result;}
}public class Test {public static void main(String[] args) {UserServiceImpl userService = new UserServiceImpl();// 关键代码UserService service = (UserService) Proxy.newProxyInstance(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces(), new CustomInvocationHandler(userService));service.add();}
}

从测试代码可以看出,Proxy类是关键。我们来看看Proxy为我们提供的方法:

Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)虽然被标注为过时方法,但是从名字上可以得知,其目的是为了获得代理类的Class对象。话不多说,我们来调用一下。

public class Test {public static void main(String[] args) {Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());System.out.println(proxyClass.getName());for (Method method : proxyClass.getDeclaredMethods()) {System.out.println(method.getDeclaringClass() + "." + method.getName() + "()");}System.out.println(Arrays.toString(proxyClass.getConstructors()));}
}

可以看到:

  • 获得的Class对象的名称为 $Proxy0
  • 定义了我们需要的 add();update();delete()方法。
  • 定义了一个有参构造方法 $Proxy0(InvocationHandler handler)

虽然没有无参构造方法,我们还是得尝试一下调用一下这个有参的构造方法,需要我们传入一个 java.lang.reflect.InvocationHandler对象

public class Test {@SneakyThrowspublic static void main(String[] args) {System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());// 获取$Proxy0(InvocationHandler handler)构造方法Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);UserService userService = (UserService) constructor.newInstance(new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println(proxy.getClass());System.out.println(method.getDeclaringClass() + "." + method.getName() + "()");return null;}});userService.add();}
}

看的出来,**当我们获得代理对象之后,通过代理对象来调用接口方法,都会回调构造时传进来的 InvocationHandler对象的 invoke(Object proxy, Method method, Object[] args)方法,**该方法有3个参数:

  • Object proxy:代表的是代理对象本身。
  • Method method:代表的是被调用的方法的Method对象。
  • Object[] args:代表的是被调用方法的参数。

可以猜测,JDK动态代理生成的代理类中,维护了InvocationHandler类的对象变量,并且在实现接口方法时,通过InvocationHandler对象调用了 invoke(Object proxy, Method method, Object[] args)方法。

System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");

不知道大家没有看到这行代码哈,当添加了这行代码之后,可以将在项目目录下保存动态创建的class文件, com/sun/proxy/$Proxy0.class

可以看到生成的代理类 $Proxy0继承自 Proxy类,并实现了 UserService接口,并且在 add()方法中通过其父类 Proxy中维护的 InvocationHandler对象调用 invoke()方法,这也就成功的解释了前面调用 userService.add()方法,会回调到invoke()方法。

这时候我们再把代码改造一下,如下:

public class CustomInvocationHandler implements InvocationHandler {// 目标对象private Object target;public CustomInvocationHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("====== 方法开始 ======");Object result = method.invoke(target, args);System.out.println("====== 方法结束 ======");return result;}
}public class Test {@SneakyThrowspublic static void main(String[] args) {UserServiceImpl target = new UserServiceImpl();Class<?> proxyClass = Proxy.getProxyClass(UserServiceImpl.class.getClassLoader(), UserServiceImpl.class.getInterfaces());Constructor<?> constructor = proxyClass.getConstructor(InvocationHandler.class);UserService userService = (UserService) constructor.newInstance(new CustomInvocationHandler(target));userService.add();}
}

这样就完成了对目标对象功能的增强,前面我们提到过 Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)

已经被标注为过时,推荐我们使用 Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)方法。

动态代理设计思想

好的,到这里,我们来总结一下JDK动态的设计思想:

使用 JDK动态代理,使得我们免去编写代理类,只需要将增强功能编写在 InvocationHandlerinvoke方法中。

4、CGLib动态代理

CGLib代理的目标对象不需要事先任何接口,它是通过动态集成目标对象实现动态代理的。CGLib代理执行代理方法的效率之所以比JDK高,是因为CGLib采用了FastClass机制:为代理类和被代理类各生成一个类,这个类会为代理类或被代理类的方法分配一个index(int类型);这个index当作一个入参,FastClass 就可以直接定位要调用的方法并直接进行调用,省去了反射调用,所以调用效率比JDK代理通过反射调用高。FastClass并不是跟代理类一起生成的,而是在第一次执行MethodProxy的invoke()或invokeSuper()方法时产生并放在缓存中的。

5、CGLib和JDK动态代理对比

  • JDK动态代理实现了被代理对象的接口,CGLib代理继承了被代理对象。
  • JDK动态代理和CGLib代理在运行期生成字节码,JDK动态代理直接写Class字节码,CGLib代理使用ASM框架(字节码操控框架)写Class字节码,CGLib代理实现更复杂,生成代理类比JDK动态代理效率低。
  • JDK动态代理调用代理方法是通过反射机制调用的,CGLib代理是通过FastClass机制直接调用方法的,CGLib代理的执行效率更高

6、Spring中的代理选择原则

  • 当Bean有实现接口时,Spring就会用JDK动态代理
  • 当Bean没有实现接口时,Spring会选择CGLib代理
  • Spring可以通过配置强制使用CGLib代理,只需要在配置中加入<aop:aspectj-autoproxy roxy-target-clas=“true”>

相关文章:

设计模式二:代理模式

1、什么是动态代理 可能很多小伙伴首次接触动态代理这个名词的时候&#xff0c;或者是在面试过程中被问到动态代理的时候&#xff0c;不能很好的描述出来&#xff0c;动态代理到底是个什么高大上的技术。不方&#xff0c;其实动态代理的使用非常广泛&#xff0c;例如我们平常使…...

php使用get_browser()函数将移动端和pc端分开

首先&#xff0c;确保你的PHP版本支持get_browser函数。get_browser函数是PHP内置的函数&#xff0c;但需要配置php.ini文件中的browscap参数&#xff0c;指定一个浏览器配置文件。 下载浏览器配置文件。你可以从 https://download.csdn.net/download/bigorange1/88850695 下…...

更改WordPress作者存档链接author和Slug插件Edit Author Slug

WordPress默认所有用户的存档永久链接都是/author/username/&#xff0c;不管是管理员还是订阅者或贡献者或作者或编辑。如果你想要自定义用户存档链接&#xff0c;比如根据角色不同使用不一样的author&#xff0c;或者自定义作者链接中的用户名Slug&#xff0c;那么建议考虑使…...

RISCV——Spinlock锁的理解

目录 1. 原子操作 2. 锁是干嘛的 3. 锁的实现 1. 原子操作 gcc从4.1.2开始提供了__sync_*系列的build-in函数&#xff0c;用于提供加减和逻辑运算的原子操作&#xff0c;主要接口的其声明如下&#xff1a; type __sync_fetch_and_add (type *ptr, type value, ...): 将valu…...

阿里云幻兽帕鲁Windows 服务器怎么下载存档?

阿里云幻兽帕鲁Windows 服务器怎么下载存档&#xff1f;通过远程连接window服务器桌面的方式。 远程连接到阿里云的 Windows 服务器后&#xff0c;可以将压缩后的存档文件&#xff0c;拖动到 workbench\Download 目录后&#xff0c;就会触发浏览器的文件下载&#xff0c;然后将…...

ArcGIS API for JavaScript 4.X 本地部署(js,字体)

0 目录&#xff08;4.19&#xff09; /4.19/ 1 修改文件 1.1 init.js 编辑器打开/4.19/init.js搜索文本[HOSTNAME_AND_PATH_TO_JSAPI]&#xff0c;然后将其连同前面的https://替换为http://ip地址/4.19&#xff0c;可以是localhost&#xff0c;只能本机引用 替换后&#xff…...

202427读书笔记|《猫的自信:治愈系生活哲学绘本》——吸猫指南书,感受猫咪的柔软慵懒与治愈

202427读书笔记|《猫的自信&#xff1a;治愈系生活哲学绘本》——吸猫指南书&#xff0c;感受猫咪的柔软慵懒与治愈 《猫的自信&#xff1a;治愈系生活哲学绘本》作者林行瑞&#xff0c;治愈系小漫画绘本&#xff0c;10分钟可以读完的一本书&#xff0c;线条明媚&#xff0c;自…...

数据结构链表力扣例题AC(2)——代码以及思路记录

206. 反转链表 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 AC方法1 struct ListNode* reverseList(struct ListNode* head) {if(head NULL)return NULL;struct ListNode* n1, *n2, *n3;n1 NULL;n2 head;n3 head->next;while…...

C++面试宝典第30题:分发饼干

题目 假设你是一位非常棒的家长,想要给你的孩子们分发一些小饼干。但是,每个孩子最多只能给一块饼干。对每一个孩子i,都有一个胃口值gi,这是能让孩子们满足胃口的饼干的最小尺寸。对每一块饼干j,都有一个尺寸sj。如果sj >= gi,我们就可以将这个饼干j分配给孩子i,这个…...

文件包含+文件上传漏洞(图片马绕过)

目录 一.文件包含二.文件上传三.图片马四.题目 一.文件包含 将已有的代码以文件形式包含到某个指定的代码中&#xff0c;从而使用其中的代码或者数据&#xff0c;一般是为了方便直接调用所需文件&#xff0c;文件包含的存在使得开发变得更加灵活和方便&#xff08;若对用户输入…...

华为配置旁挂二层组网隧道转发示例

配置旁挂二层组网隧道转发示例 组网图形 业务需求组网需求数据规划配置思路配置注意事项操作步骤配置文件扩展阅读 业务需求 企业用户通过WLAN接入网络&#xff0c;以满足移动办公的最基本需求。且在覆盖区域内移动发生漫游时&#xff0c;不影响用户的业务使用。 组网需求 AC组…...

Postgresql源码(123)事务提交时三段资源释放分析ResourceOwnerRelease

0 总结 三段释放原因&#xff1a;因为如果先释放锁&#xff0c;没有释放一些共享资源&#xff08;比如pin住的buffer&#xff09;&#xff0c;别人拿到锁后发现我们仍然持有一些资源&#xff0c;就会有问题。所以三阶段释放主要是以锁为分界线&#xff0c;先释放锁保护的资源&…...

电脑文件误删除如何恢复?2024最新三种恢复方法

我们在使用电脑的过程中&#xff0c;随着时间的不断推移&#xff0c;渐渐的我们会发现C盘内存空间不足了。这是因为我们很多文件都默认存储在C盘&#xff0c;所以导致C盘空间不足&#xff0c;电脑运行越来越慢。那么电脑哪些文件可以删除&#xff0c;电脑删除的东西怎么恢复&am…...

Netty应用——Google Protobuf强化篇(二十)

Protobuf发送一种实例 客户端可以发送一个 Student PoJo 对象到服务器 (通过 Protobuf 编码)服务端能接收 Student PoJo 对象&#xff0c;并显示信息(通过 Protobuf 解码) Student.proto syntax "proto3"; //版本 option java_outer_classname "StudentPOJO&…...

SpringAMQP开启“可靠性”机制

前言 上一篇介绍了如何在 《SpringBoot 中集成和使用消息队列》&#xff0c;看过这一篇就基本上可以在SpringBoot中使用消息队列了&#xff0c;但是消息队列他归根结底是一个客户端服务器模式的中间件&#xff0c;面对复杂的网络环境和分布式使用环境&#xff0c;难免会出现各…...

戴尔Dell R740服务器开机冒烟亮黄灯故障维修

今天分享的是一台过保修期的DELL PowerEdge R740服务器开机冒烟的维修案例。先上图&#xff1a; 接到用户报修后工程师立即响应&#xff0c;由于用户也是刚开工第一天服务器开机就出现了这种祥龙吐雾的祥兆&#xff0c;导致工厂业务流程无法正常使用&#xff0c;这台机器在东莞…...

【阅读笔记】空域保边降噪《Side Window Filtering》

1、保边滤波背景 保边滤波器的代表包括双边滤波、引导滤波&#xff0c;但是这类滤波器有一个问题&#xff0c;它们均将待处理的像素点放在了方形滤波窗口的中心。但如果待处理的像素位于图像纹理或者边缘&#xff0c;方形滤波核卷积的处理结果会导致这个边缘变模糊。 基于这个…...

vue3前端excel导出;组件表格,自定义表格导出;Vue3 + xlsx + xlsx-style

当画面有自定义的表格或者样式过于复杂的表格时&#xff0c;导出功能可以由前端实现 1. 使用的插件 &#xff1a; sheet.js-xlsx 文档地址&#xff1a;https://docs.sheetjs.com/ 中文地址&#xff1a;https://geekdaxue.co/read/SheetJS-docs-zh/README.md xlsx-style&#…...

npm install一直卡在 sill idealTree buildDeps

当npm install命令在安装过程中卡在sill idealTree buildDeps阶段时&#xff0c;可能的原因包括网络延迟、镜像源问题或缓存问题。以下是一些可能的解决方法&#xff1a; 检查镜像源&#xff1a; 打开命令提示符&#xff08;cmd&#xff09;窗口。 输入命令npm config get…...

spring boot rabbitmq常用配置

直接上代码 package com.example.demo;import org.aopalliance.aop.Advice; import org.springframework.amqp.rabbit.annotation.RabbitListenerConfigurer; import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; import org.springframewo…...

SciencePlots——绘制论文中的图片

文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了&#xff1a;一行…...

【Go】3、Go语言进阶与依赖管理

前言 本系列文章参考自稀土掘金上的 【字节内部课】公开课&#xff0c;做自我学习总结整理。 Go语言并发编程 Go语言原生支持并发编程&#xff0c;它的核心机制是 Goroutine 协程、Channel 通道&#xff0c;并基于CSP&#xff08;Communicating Sequential Processes&#xff0…...

【git】把本地更改提交远程新分支feature_g

创建并切换新分支 git checkout -b feature_g 添加并提交更改 git add . git commit -m “实现图片上传功能” 推送到远程 git push -u origin feature_g...

在鸿蒙HarmonyOS 5中使用DevEco Studio实现录音机应用

1. 项目配置与权限设置 1.1 配置module.json5 {"module": {"requestPermissions": [{"name": "ohos.permission.MICROPHONE","reason": "录音需要麦克风权限"},{"name": "ohos.permission.WRITE…...

深度学习水论文:mamba+图像增强

&#x1f9c0;当前视觉领域对高效长序列建模需求激增&#xff0c;对Mamba图像增强这方向的研究自然也逐渐火热。原因在于其高效长程建模&#xff0c;以及动态计算优势&#xff0c;在图像质量提升和细节恢复方面有难以替代的作用。 &#x1f9c0;因此短时间内&#xff0c;就有不…...

MySQL 索引底层结构揭秘:B-Tree 与 B+Tree 的区别与应用

文章目录 一、背景知识&#xff1a;什么是 B-Tree 和 BTree&#xff1f; B-Tree&#xff08;平衡多路查找树&#xff09; BTree&#xff08;B-Tree 的变种&#xff09; 二、结构对比&#xff1a;一张图看懂 三、为什么 MySQL InnoDB 选择 BTree&#xff1f; 1. 范围查询更快 2…...

tomcat指定使用的jdk版本

说明 有时候需要对tomcat配置指定的jdk版本号&#xff0c;此时&#xff0c;我们可以通过以下方式进行配置 设置方式 找到tomcat的bin目录中的setclasspath.bat。如果是linux系统则是setclasspath.sh set JAVA_HOMEC:\Program Files\Java\jdk8 set JRE_HOMEC:\Program Files…...

Ubuntu系统多网卡多相机IP设置方法

目录 1、硬件情况 2、如何设置网卡和相机IP 2.1 万兆网卡连接交换机&#xff0c;交换机再连相机 2.1.1 网卡设置 2.1.2 相机设置 2.3 万兆网卡直连相机 1、硬件情况 2个网卡n个相机 电脑系统信息&#xff0c;系统版本&#xff1a;Ubuntu22.04.5 LTS&#xff1b;内核版本…...

AD学习(3)

1 PCB封装元素组成及简单的PCB封装创建 封装的组成部分&#xff1a; &#xff08;1&#xff09;PCB焊盘&#xff1a;表层的铜 &#xff0c;top层的铜 &#xff08;2&#xff09;管脚序号&#xff1a;用来关联原理图中的管脚的序号&#xff0c;原理图的序号需要和PCB封装一一…...

【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅!

【把数组变成一棵树】有序数组秒变平衡BST,原来可以这么优雅! 🌱 前言:一棵树的浪漫,从数组开始说起 程序员的世界里,数组是最常见的基本结构之一,几乎每种语言、每种算法都少不了它。可你有没有想过,一组看似“线性排列”的有序数组,竟然可以**“长”成一棵平衡的二…...