news 2026/9/9 19:28:23

SLF4J与Logback日志体系实战:依赖冲突与异步队列排坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SLF4J与Logback日志体系实战:依赖冲突与异步队列排坑指南

如果你维护过稍微有点年头的 Java 项目,大概率见过类似场景:某个晚上发布新版本,服务起来后控制台直接刷出一行“SLF4J: Class path contains multiple SLF4J bindings”,接着到处是NoSuchMethodError,接口超时,日志却一片空白。大多数人第一反应是“清缓存重启”,但问题往往出在最容易被忽略的日志依赖上。

SLF4J 和 Logback 在 Java 生态里几乎是标准配置。SLF4J 是日志门面,它本身不干活,只定义 API;Logback 才是真正输出日志的实现。两者组合的好处是:业务代码只依赖 SLF4J,底层框架随便切换,不会因为日志实现不同而把代码改一遍。这套体系看起来简单,实际落地时坑很深,尤其是依赖冲突、多文件输出、异步队列丢日志这几类问题。这篇文章从原理讲到配置,再从配置讲到排错,尽量把我在实际项目里踩过的坑一次说清楚。不管你是刚接触 Java 日志的新手,还是被日志问题折磨过的老手,应该都能找到有用的东西。

1. 为什么现在的 Java 日志体系这么绕:门面与实现的来龙去脉

先说一个结论:Java 日志从来不是“选一个框架就行”,而是“门面 + 实现 + 桥接”三者一起工作。很多人只知道 SLF4J 和 Logback,以为把它们加进 pom 就行了。实际上一个大型项目里可能同时存在 Commons Logging、Log4j2、Java Util Logging 的调用代码,这些都得通过桥接器统一到 SLF4J 上来,否则日志会乱套。

1.1 从 System.out 到日志框架的演进

最原始的日志方式就是System.out.println()。它的好处是零成本,坏处也很明显:无法分级、无法控制输出位置、难以在运行时开关。后来 Java 官方提供了java.util.logging(JUL),功能有了,但 API 别扭、性能一般,实际用得不多。再后来 Log4j 1.x 火了很长时间,几乎是 Java 日志的事实标准。但 Log4j 1.x 停在 1.2.17 很久,性能和维护都跟不上,于是 Logback 出现,继承了 Log4j 的设计理念,性能更好,原生支持 SLF4J。现在还有 Log4j2,性能也很强,但接口和配置风格不同,生态里同样占一席之地。

问题在于:项目里引的第三方库不可能都用同一个日志框架。有的用 Commons Logging,有的用 Log4j,有的直接写 JUL。如果没有统一方案,每个框架自己写自己的文件,排查问题时要看十几个日志文件,而且日志配置互相覆盖,非常痛苦。

1.2 SLF4J 到底做了什么

SLF4J(Simple Logging Facade for Java)是一个门面,它做的事情就是给调用方一套统一 API:

private static final Logger logger = LoggerFactory.getLogger(UserService.class); logger.info("用户 {} 登录成功", userId);

注意这个用法和 Log4j 1.x 的区别:SLF4J 用花括号占位符,不支持字符串拼接那种用法。logger.info("用户:" + userId)也能跑,但会在参数传入前就完成拼接,白做一次字符串操作,日志级别不满足时这开销完全是浪费。占位符方案则能延迟到真正需要输出时才格式化。

当你的代码只引用slf4j-api时,它自己不会输出任何东西。真正干活的是运行时绑定到 classpath 上的实现,比如logback-classic。SLF4J 会通过ServiceLoader或静态绑定机制找到一个可用的LoggerProvider,把它包装成自己的 Logger 返回给调用方。这个“运行时绑定”的设计是 SLF4J 的核心,也是很多依赖冲突的根源。

1.3 绑定器与桥接器的对应关系

要理解日志体系,必须分清两组东西:绑定器和桥接器。

绑定器是“实现方适配 SLF4J 的入口”。Logback 的logback-classic本身就是 SLF4J 的绑定器。Log4j2 则通过log4j-slf4j-impl做绑定。JUL 需要通过slf4j-jdk14绑定。如果 classpath 里出现多个绑定器,SLF4J 会报警告并随便选一个,这就是“multiple SLF4J bindings”的由来。

桥接器是“让老框架的调用转到 SLF4J 上来”。比如第三方库用的是 Log4j 1.x,为了让它的日志也走 Logback 输出,需要引入log4j-over-slf4j。这个桥接器里有一批和org.apache.log4j同名的类,但内部实现全部转发到 SLF4J。同理,jcl-over-slf4j处理 Commons Logging,jul-to-slf4j处理 JUL。

一张表看清楚:

依赖作用什么时候用
slf4j-api门面 API业务代码直接依赖
logback-classicSLF4J 绑定器 + 日志实现最终输出日志
logback-coreLogback 底层间接依赖,不用直接引
log4j-over-slf4jLog4j 1.x 到 SLF4J 桥接项目里还有老 Log4j 调用
jcl-over-slf4jCommons Logging 到 SLF4J 桥接依赖了 Spring 等老框架
jul-to-slf4jJUL 到 SLF4J 桥接需要统一 JDK 自带日志
log4j-slf4j-implLog4j2 绑定 SLF4J想用 Log4j2 做实现时

有一个很容易踩的坑:桥接器和绑定器不能同时放。比如log4j-over-slf4jslf4j-log4j12如果同时存在,就会形成循环调用:前者把 Log4j 调用转到 SLF4J,后者又把 SLF4J 调用转回 Log4j,最终StackOverflowError。我见过有人在排查依赖冲突时,想“都加上总有一个能用”,结果把这两组都引进去,线上直接炸。这类问题不是看报错就能立刻发现的,需要打开实际依赖树去检查。

2. Logback 的核心组件与配置骨架

Logback 的设计比 Log4j 1.x 清晰,核心概念只有三个:Logger、Appender、Layout。理解这三者的关系,配置就不会晕。

2.1 Logger、Appender、Layout 三者关系

Logger 是日志记录器,负责决定“这条日志要不要打印”。它有一个树状继承结构,最顶层是根 Logger。子 Logger 会继承父 Logger 的级别和 Appender,但也可以用additivity控制是否继续往父级传递。

Appender 是输出目标,可以输出到控制台、文件、数据库、Kafka 等。一个 Logger 可以挂多个 Appender,最终日志会同时写到所有目标。

Layout 负责格式化日志内容。Logback 里更常用的是Encoder,区别在于 Layout 是把事件转成字符串,Encoder 还能控制写出方式。从 Logback 0.9.19 之后官方推荐用PatternLayoutEncoder

三者配合的流程:业务代码通过Logger.info()抛出一条日志事件,Logger 判断级别是否满足,满足后交给当前 Logger 关联的 Appender,Appender 再调用 Encoder 把格式化成字符串输出。

所以配置日志本质就是三件事:设置日志级别、配置 Appender、定义输出格式。

2.2 logback.xml 的最小可用配置

一个纯 Logback 项目,classpath 下有logback.xml就能跑。最小配置长这样:

<configuration> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> <root level="INFO"> <appender-ref ref="CONSOLE"/> </root> </configuration>

%d是时间,%-5level是级别,%thread是线程名,%logger{36}是 Logger 名称缩写,%msg是消息,%n是换行。这套 pattern 是标配,直接抄没问题。

这里有个容易被忽略的点:logger名称通常用Class.getName(),所以在代码里写LoggerFactory.getLogger(XXX.class)是正确的做法。有些人图省事用字符串,比如LoggerFactory.getLogger("user"),短期能用,但后面想做包级别过滤时会很别扭。

2.3 配置文件加载顺序和常见坑

Logback 加载配置文件的顺序是:先找logback-test.xml,再找logback.groovy,最后找logback.xml。都不存在就用内置的BasicConfigurator,也就是控制台输出 INFO 级别的默认行为。

这一点很多人不清楚,导致一个诡异现象:测试时日志配置生效,生产环境却不生效。因为测试目录下放了logback-test.xml,它把logback.xml覆盖了。还有更隐蔽的:项目里有两个logback.xml,分别在不同的 jar 包中,classpath 加载顺序决定谁生效,问题很难复现。

解决办法是在启动参数里加-Dlogback.debug=true,Logback 启动时会把配置加载过程打印到控制台,包括“找到了哪个配置文件”“用了哪个 Appender”。排查加载问题第一步就是开这个开关。

另外,Spring Boot 环境的加载顺序不一样。Spring Boot 优先使用logback-spring.xml,而不是logback.xml。如果你在 Spring Boot 项目里写logback.xml,也能用,但<springProfile>这类标签会失效。所以我建议:Spring Boot 项目统一用logback-spring.xml,纯 Java 项目用logback.xml

3. 从零配置一个可落地的日志方案:多文件、滚动、异步

配置能跑和配置能扛是两码事。生产环境的日志方案至少要满足三个要求:按级别/模块拆文件、日志自动滚动清理、降低日志对业务线程的影响。下面给一套我实际在项目里用的配置骨架。

3.1 选定依赖版本与 scope

Maven 依赖最基本的一组:

<dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.36</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.13</version> <scope>runtime</scope> </dependency>

logback-classic的 scope 设为runtime是推荐做法,因为业务代码不应该直接依赖 Logback 的类。如果哪天你想换成 Log4j2,只要删掉这个依赖、换成log4j-slf4j-impl,业务代码一行不用改。这就是门面的好处。

需要注意版本配套:SLF4J 1.7.x 对应 Logback 1.2.x,SLF4J 2.x 对应 Logback 1.4.x,不能随便混。如果你看到java.lang.NoSuchMethodError: org.slf4j.LoggerFactory.getILoggerFactory,多半就是 slf4j-api 和 logback-classic 版本没对齐。

3.2 多文件输出配置

很多项目需要把业务日志和框架日志分开,比如把com.example.order包的日志写到order.log,其他日志写到app.log。配置如下:

<configuration> <property name="LOG_HOME" value="/data/logs/myapp"/> <appender name="APP_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/app.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/app.%d{yyyy-MM-dd}.log.gz</fileNamePattern> <maxHistory>30</maxHistory> <totalSizeCap>10GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> <appender name="ORDER_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_HOME}/order.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>${LOG_HOME}/order.%d{yyyy-MM-dd}.log.gz</fileNamePattern> <maxHistory>30</maxHistory> <totalSizeCap>10GB</totalSizeCap> </rollingPolicy> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="com.example.order" level="DEBUG" additivity="false"> <appender-ref ref="ORDER_FILE"/> </logger> <root level="INFO"> <appender-ref ref="APP_FILE"/> </root> </configuration>

关键点有两个:com.example.order这个 Logger 设了additivity="false",这样它的日志不会继续传给 root,避免 order 日志在 app.log 里再出现一次。如果不设置,会出现“一份日志写两遍”的情况,看起来不致命,但文件体积翻倍,排查问题时也很困惑。

fileNamePattern里的.gz后缀是 Logback 自带压缩。当日志滚动时,前一天的日志会自动压缩成 gzip,节省磁盘。maxHistory控制保留多少天,totalSizeCap控制所有历史文件的总大小,防止日志无限膨胀。生产环境这两个参数一定要配,不配的话磁盘迟早被撑爆。

3.3 滚动策略与异步写日志

RollingFileAppender 的滚动策略按时间切文件是推荐的默认做法。但在高并发下,直接往磁盘写日志会让业务线程阻塞在 IO 上。Logback 提供了AsyncAppender,它内部维护一个队列,业务线程只把日志事件丢进队列就返回,后台线程负责真正写盘。

<appender name="ASYNC_APP" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>8192</queueSize> <discardingThreshold>0</discardingThreshold> <neverBlock>true</neverBlock> <appender-ref ref="APP_FILE"/> </appender>

这几个参数一定得理解透彻,不然会出“日志静默丢失”的雷。queueSize是队列容量,默认 256,对于高并发服务偏小。discardingThreshold是丢弃阈值,默认是队列容量的 20%,意思是当队列剩余容量低于 20% 时,为了保吞吐,会直接丢弃 TRACE/DEBUG/INFO 级别的事件,只保留 WARN/ERROR。如果你很在意完整日志,把它设为 0,表示队列满了也不主动丢低级别日志。neverBlock设为 true 表示队列满了不阻塞业务线程,直接丢弃新事件。这个参数能保证业务性能,但代价是日志可能丢。

我个人的建议是:核心业务日志用neverBlock=false+ 大队列,宁可让写日志慢一点也不能丢关键链路日志;辅助类日志用neverBlock=true,丢了也无所谓。很多团队无脑把所有日志都改成 neverBlock=true,后来排查线上问题时发现 ERROR 日志都丢了,那是最痛苦的场景。

4. 实际项目里 SLF4J 和 Logback 最容易踩的坑

这一节全是真实踩坑记录。日志框架本身不难,难的是它藏在依赖树的深处,报错信息又经常很迷惑。

4.1 依赖冲突:NoSuchMethodError 与 StackOverflow

先看最常见的NoSuchMethodError。日志框架升级后,老代码用旧版本的类,新版本把方法移走或改名,JVM 加载时找不到方法就会抛这个错。比如org.slf4j.LoggerFactory.getILoggerFactory()在 SLF4J 2.x 里已经不建议使用,如果某个老库还在调用,而 classpath 里是新版本,就会报NoSuchMethodError

还有一种更隐蔽的:项目里引入log4j-over-slf4j后,某个第三方库内部用了 Log4j 的NDC或者Category,但桥接器只实现了部分类,导致ClassNotFoundException。解决方案不是去掉桥接器,而是升级那个老库,或者把调用的那部分代码隔离。

最麻烦的是StackOverflowError。如果同时存在绑定器和反向桥接器,比如slf4j-log4j12log4j-over-slf4j,每次打日志都在两个框架之间循环调用,最后栈溢出。检查方法是用mvn dependency:tree搜关键字:

mvn dependency:tree -Dincludes=org.slf4j:*,log4j:*,ch.qos.logback:*

看到slf4j-log4j12log4j-over-slf4j同时出现,基本可以断定有问题。原则是“绑定器只留一个,桥接器方向要一致”。如果你的最终实现是 Logback,那就只留logback-classic一个绑定器,所有老框架都通过桥接器转到 SLF4J。

4.2 日志丢失:异步队列满了之后

很多团队启用了 AsyncAppender 之后,隔段时间发现日志文件变小了,还以为是业务量下降。真相往往是队列满了,日志被丢弃。

异步日志的三个参数需要根据业务量调:queueSizediscardingThresholdneverBlock。我见过一个支付系统,某个时段日志洪峰特别大,队列 256 瞬间塞满,INFO 日志几乎全被丢,问题定位全靠 ERROR 日志。但那段时间 ERROR 日志也变少了,因为队列满时 ERROR 一样进不去。

调优建议:先用压测模拟峰值流量,观察AsyncAppender的丢弃情况。Logback 的 JMX 可以查看队列剩余容量,也可以把队列大小调大后观察。一般 8192 起步,如果仍不够说明后台写入速度跟不上,需要检查磁盘性能或减少单条日志大小。

另外要注意:AsyncAppender 里面放一个 RollingFileAppender 时,immediateFlush参数要谨慎设置。Logback 默认每次写日志都 flush 到磁盘,这会拖慢异步线程。可以设immediateFlush=false,让输出流缓冲到一定程度再 flush,性能更好,但进程突然崩溃时会丢最后一批缓冲数据。取舍要看业务敏感性。

4.3 pattern 里的性能陷阱

日志格式化看着简单,其实很影响性能。%class%method%line这几个转换符需要获取调用栈信息,代价非常大。在高并发下,一行日志里如果带了这些字段,费用高的吓人。默认的%logger是从 Logger 名称来的,不需要额外获取调用栈,所以建议尽量少用%class%method%line

还有异常堆栈的写法。很多人写:

<pattern>%d{HH:mm:ss} %-5level [%thread] %logger - %msg%n</pattern>

单看没问题,但遇到logger.error("xxx", exception)时,Logback 默认会把堆栈整个打出来,堆栈多的日志单条能达到几十 KB。如果一天有几次大规模的异常重试,日志文件会暴涨。想要限制堆栈打印长度,可以加:

<pattern>%d{HH:mm:ss} %-5level [%thread] %logger - %msg%n%ex{3}</pattern>

%ex{3}表示只打印堆栈前 3 行,避免把整个堆栈全打出来。这在排查问题时其实更好用,因为前几行已经能定位到异常类型和位置,后面的堆栈大多是框架内部的底层调用,信息量低。

4.4 敏感信息与日志污染

日志里打敏感信息是个老生常谈的问题。logger.info("用户信息:{}", user)如果 user 的 toString() 里包含手机号、身份证、密码字段,日志文件就是一颗定时炸弹。常见做法是在实体类的 toString() 里手动脱敏,或者用 Logback 的Replace正则过滤器:

<encoder> <pattern>%msg%n</pattern> <replace> <regex>(\d{11})</regex> <replacement>$1</replacement> </replace> </encoder>

不过正则替换也有代价,大流量下会拉低吞吐。更稳妥的方案是在写入前就在业务层把敏感字段脱敏。我见过有些团队把脱敏逻辑写在日志里,结果日志量一大,CPU 飙高。脱敏这种事,越靠源头做越划算。

另一个容易被忽略的是日志污染:只要异常堆栈就打 ERROR 级别。有些异常是业务流程的一部分,比如用户取消订单、请求超时,这种属于预期内的分支,打 WARN 就行,打 ERROR 会让监控系统频繁误报,最后大家看到 ERROR 就麻木,真正的问题反而不被关注。日志级别代表严重程度,不是所有异常都值得 ERROR。

5. 从问题现场到根因:一次实际排错过程复盘

说了这么多理论,分享一下我最近一次处理日志问题的完整链路。现象是服务正常运行,但订单模块的关键日志时有时无,ERROR 级别偶尔能看到,INFO 级别的链路一直不完整。

第一步,开-Dlogback.debug=true重启,确认加载的配置文件是预期的logback-spring.xml。这一步排除了配置加载顺序问题。

第二步,查看配置里订单模块的 logger。发现订单模块 Logger 的level被设置成了 WARN,但开发环境是 DEBUG。问题初步定位到配置分发:测试环境用的是logback-spring.xml,生产环境挂载的配置卷里有一份旧的logback.xml,它把com.example.order级别覆盖成了 WARN。这就是为什么配置文件加载顺序那么敏感。

第三步,检查异步队列。生产配置里queueSize=256discardingThreshold=128,当时正赶上大促压测,日志洪峰直接把队列塞满。虽然neverBlock=false,但阈值逻辑让大量 INFO 被丢弃。这是链路日志不完整的主要原因。

最后把queueSize调到 8192,discardingThreshold改成 0,并针对订单模块单独开了独立的异步 Appender,避免和其他业务日志互相挤占。问题解决后,我又顺手把logback.xmllogback-spring.xml的优先级写进了团队文档,明确 Spring Boot 项目不允许再用logback.xml,避免二次踩坑。

整个过程没有高深的技术,就是按顺序排查:配置文件加载没、加载的是哪个、级别对不对、异步队列丢没丢。日志问题十有八九出在这四个环节。建议你遇到类似现象,先按这个顺序过一遍,比蒙头重启高效得多。

6. 补充几个可以立刻用起来的小技巧

最后给几条我在实际项目里验证过有效的经验,不算系统知识,但能省不少事。

第一,日志配置文件里尽量用property把路径、格式、保留天数收拢到顶部。项目换服务器目录或者统一调整格式时,改一处就行。否则几十个 Appender 散落着绝对路径,改起来非常痛苦。

第二,利用 MDC 做链路追踪。在过滤器或拦截器里把请求 ID 放进 MDC:

MDC.put("traceId", requestId); try { chain.doFilter(request, response); } finally { MDC.remove("traceId"); }

然后在 pattern 里加[%X{traceId}],所有日志都会自动带上这个 ID。排查一个请求跨多个模块的调用链时,用 traceId 一 grep 全出来了。比看时间戳猜日志可靠得多。

第三,上线前用mvn dependency:tree检查一遍日志相关依赖,重点看有没有多个绑定器、桥接器方向是否一致。这个动作只要几十秒,但能避免大部分日志依赖问题。很多线上事故都是改了一个依赖版本后引发的,依赖树检查应该进入发布流程。

第四,日志级别可以运行时动态调整。Logback 提供了 JMX MBean,不用重启就能改某个包的日志级别。真出问题时,先临时把相关包的级别降到 DEBUG,拿到现场日志后再恢复。这个操作在关键时刻能救命,但要注意别在生产环境开着 DEBUG 跑太久,日志量会失控。

SLF4J 和 Logback 这套组合本身非常成熟,出问题基本都是使用方式的问题。把门面和实现的关系理清,把配置文件的加载顺序记住,再对异步队列的参数保持敏感,日志这块就能稳定很多。我自己踩过几次坑之后,现在写日志配置的第一反应不是“跑起来就行”,而是“挂掉之后能不能从日志里还原现场”。这个思路转变,比任何技巧都重要。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 19:27:29

Moby Engine API 版本怎么选?如何查看各版本变更并固定 API 版本

Moby Engine API 版本怎么选&#xff1f;如何查看各版本变更并固定 API 版本 【免费下载链接】moby The Moby Project - a collaborative project for the container ecosystem to assemble container-based systems 项目地址: https://gitcode.com/GitHub_Trending/mo/moby …

作者头像 李华
网站建设 2026/9/9 19:26:47

Marlin 固件稳定性验证:温度、限位与断料检测的实操路径

Marlin 固件稳定性验证&#xff1a;温度、限位与断料检测的实操路径 【免费下载链接】Marlin Marlin is a firmware for RepRap 3D printers optimized for both 8 and 32 bit microcontrollers. Marlin supports all common platforms. Many commercial 3D printers come with…

作者头像 李华
网站建设 2026/9/9 19:26:42

程序员考公:降维打击还是围城自困?一份真实上岸与备考指南

考公这件事&#xff0c;在程序员圈子里早就不是“个别想法”&#xff0c;而是一个被反复讨论的备选项。我身边不少写代码的朋友&#xff0c;工作三五年后&#xff0c;都或多或少动过考公的念头。有人把它叫作“降维打击”——觉得以程序员的逻辑能力和刷题功底&#xff0c;去应…

作者头像 李华
网站建设 2026/9/9 19:25:37

A*路径规划算法详解:Matlab栅格地图仿真与代码实现

一边是入口&#xff0c;一边是出口&#xff0c;中间是隔断纵横的墙——当你站在一座巨型迷宫面前&#xff0c;你会怎么找到那条最短的出路&#xff1f;如果让你把这种“找路”的直觉翻译成计算机能执行的指令&#xff0c;又要怎么设计&#xff0c;才能既保证找到最短路径&#…

作者头像 李华
网站建设 2026/9/9 19:24:08

Android广播机制全解析:标准/有序/动态/静态注册一次理清

这篇是安卓基础系列的第23篇&#xff0c;继续聊广播。前几篇我们把Activity、Service、Fragment这些大块头都过了一遍&#xff0c;到了广播这里&#xff0c;好多初学者会卡在一个问题上&#xff1a;广播到底分几种&#xff1f;什么时候用哪种&#xff1f;为什么有时候我在清单文…

作者头像 李华
网站建设 2026/9/9 19:22:45

找位置:哈希映射与顺序输出的字符串处理技巧

刷题时遇到编号 3610 的“找位置”&#xff0c;第一反应是“这题名字取得也太朴素了”。等真正动手做了才发现&#xff0c;它几乎是字符串处理里最典型的一类题&#xff1a;给你一串字符&#xff0c;找出出现次数超过一次的字符&#xff0c;并且把每个字符出现过的所有位置都输…

作者头像 李华