尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java字符串包含判断:从API原理到性能优化与实战避坑

Java字符串包含判断:从API原理到性能优化与实战避坑 在Java日常开发里“判断字符串是否包含某个子串”大概是出现频率最高的操作之一。不管是接口参数校验、URL路由匹配、日志关键词过滤还是做敏感词拦截几乎每个项目都绕不开这一行代码。网上相关教程多如牛毛但多数只停留在“会用 contains 和 indexOf”的层面真正遇到空指针、大小写问题、正则回溯陷阱、性能瓶颈时很多人还是容易懵。这篇文章算是我在整理项目代码时的一份总结也是之前和团队小伙伴做 Code Review 时反复讨论过的话题。我把它完整梳理出来包含各类 API 的底层逻辑、性能差异、典型业务场景下的选型建议以及我实际踩过的几个坑。不管是你准备面试还是想在项目里写出更稳的字符串判断代码都能从里面找到些有用的东西。1. 字符串包含判断的基础API盘点很多初学者写字符串包含判断第一反应就是用contains。其实 Java 里处理这类需求的方法远不止一个不同方法适用的场景差异很大。我先把常用的几个 API 列出来然后逐个分析它们的行为差异。1.1 contains方法String.contains(CharSequence s)是 Java 5 引入的方法用于判断当前字符串是否包含指定的字符序列。用法非常简单String url https://example.com/api/user/list; boolean result url.contains(/api/); System.out.println(result); // true这里有一个很多人容易忽略的细节contains接收的参数类型是CharSequence而不是String。这意味着你可以传StringBuilder、StringBuffer或者其他实现了CharSequence接口的自定义对象进去。不过正因为参数声明的是接口类型方法内部第一步会将该参数强转为String也就是调用toString()的结果所以你传入一个不断变化的StringBuilder时判断结果取决于方法执行那一刻的字符串内容。还有一点要知道contains是区分大小写的。Hello.contains(hello)返回false这在英文关键词匹配时常常让人意外。如果需要忽略大小写得先调用toLowerCase()或toUpperCase()但这样会额外创建一个新字符串对象在高频调用时会有内存和性能开销。后面我会重点讲如何更优雅地处理这个问题。1.2 indexOf方法indexOf(String str)返回子串在当前字符串中第一次出现的位置索引如果找不到则返回-1。这个方法比contains更“底层”一点事实上contains的内部实现就是直接调用indexOfpublic boolean contains(CharSequence s) { return indexOf(s.toString()) 0; }所以contains本质上是一个语法糖帮你隐藏了判断-1的细节。但indexOf的价值不止于此String text Java中文社区Java编程; int firstIndex text.indexOf(Java); int secondIndex text.indexOf(Java, firstIndex 1); System.out.println(firstIndex); // 0 System.out.println(secondIndex); // 7第二个参数fromIndex可以指定从哪个位置开始查找这在需要查找子串所有出现位置时就非常重要。比如统计某关键词在文本中出现了多少次或者获取所有匹配位置的偏移量做高亮显示indexOf比contains更适合。我经常在日志分析场景中用这个方式做命中定位。1.3 startsWith、endsWith与regionMatchesstartsWith(String prefix)判断字符串是否以某个前缀开头endsWith(String suffix)判断是否以某个后缀结尾。它们的使用频率也很高特别是在做文件名后缀校验、协议前缀匹配时。String filename report_20240115.pdf; boolean isPdf filename.endsWith(.pdf); boolean hasPrefix filename.startsWith(report_);如果既要忽略大小写又要判断前缀是否匹配startsWith也提供了重载形式startsWith(String prefix, int toffset)即从指定偏移位置开始匹配。不过我自己更常用regionMatches来处理更灵活的比对String source ABCDefg; String target cde; boolean ignoreCaseMatched source.regionMatches(true, 2, target, 0, 3); System.out.println(ignoreCaseMatched); // trueregionMatches的五个参数分别是是否忽略大小写、源字符串起始偏移、目标字符串、目标字符串起始偏移、比较长度。当你需要比较字符串某个“区间片段”时它会比先substring再equalsIgnoreCase少创建很多中间对象。涉及大量片段比较的时候这个性能优势会被放大。1.4 matches方法matches(String regex)判断整个字符串是否完全匹配给定的正则表达式注意是“完全匹配”而不是“包含匹配”。String phone 13812345678; boolean isPhone phone.matches(^1[3-9]\\d{9}$);这里的坑在于如果只是想判断“字符串里是否含有一个符合规则的子串”直接用matches是做不到的。你需要借助Pattern.compile(regex).matcher(text).find()或者find()方法。很多初学者在这里栽过跟头——用了matches来判断“包含数字”结果只要字符串里有任何非数字字符就返回false。String text 订单号: A123B456; // 错误示范matches 要求全串匹配 System.out.println(text.matches(\\d)); // false // 正确方式find 判断存在性 Pattern pattern Pattern.compile(\\d); System.out.println(pattern.matcher(text).find()); // true另外每次调用matches都会重新编译一次正则表达式如果在一个循环里执行性能损耗明显。更科学的做法是把Pattern定义为静态常量或类级常量只需要编译一次然后反复复用。代码规范上我也见过不少团队把这个写进了检查规则。2. 底层实现原理与性能差异分析2.1 从源码看contains和indexOf的关联先看 OpenJDK 中String.contains的源码实现public boolean contains(CharSequence s) { return indexOf(s.toString()) 0; }可见它直接委托给indexOf。因此你不需要同时用两个方法做判断调用contains并不会产生额外的高昂开销它的性能与indexOf几乎一致。但有个细微差别contains内部会对传入的CharSequence调用toString()。如果传入的是自定义CharSequence且toString()每次都会新建字符串那性能就会受到影响。indexOf在 JDK 中针对char[]做了多种优化。较新版本的 JDK 对String内部存储使用了byte[]加编码标志COMPACT_STRINGS和LATIN1/UTF16因此在判断纯 ASCII 字符串时indexOf走的是一套更快的字节比较逻辑。只有当字符串包含中文字符或其他宽字符时才会切换到 UTF-16 的比较路径。这也是为什么你在不同字符集的数据上测试contains耗时会有细微区别。indexOf的默认实现是朴素匹配算法从目标字符串的第一个字符开始和模式串的第一个字符对齐逐个比较如果不匹配就整体向右移动一位继续比。最坏时间复杂度是 O(n*m)n 是主串长度m 是模式串长度。在绝大多数业务场景下字符串都不算太长这个性能完全够用。但如果你在做一个需要高频匹配大量文本的系统比如文本搜索引擎、日志采集过滤器朴素匹配可能就不够用了。2.2 为什么很多场景不需要过度优化很多性能优化的文章会告诉你“JDK 的 indexOf 没有用 KMP性能不好要用就自己实现”。这种说法放在大多数业务系统中其实站不住脚。我统计过一次在一个中等规模的订单系统中做的测试一万次contains判断主串平均长度 200 字符模式串平均长度 10 字符总耗时大约在 1 到 3 毫秒之间。对绝大多数 Web 接口来说这种开销完全可以忽略不计。在数据量没上去之前过度追求算法优化反而是技术债。你用 KMP 或 Boyer-Moore 实现一个自定义的寻找函数代码复杂度上升了维护成本也增加而收益却看不见。只有当单个主串长度超过几千字符且模式串固定并需要大量重复匹配时才有必要考虑更高效的模式匹配算法。这时候我都建议优先尝试 JDK 自带的工具类比如java.util.regex.Pattern的预编译模式很多场景下它的匹配速度已经优于朴素的indexOf了。我还见过一种还算实用的方案如果模式串集合固定且数量有限可以预先构建一个Aho-Corasick自动机来做多模式匹配。这种方式适合敏感词过滤或关键词统计等“一对多”的场景。但一样业务量不达标就别折腾直接for循环调用contains就行代码可读性高得多。2.3 匹配算法选型建议什么情况用什么我把匹配场景做了一个简单的分类这样在技术评审时可以快速对齐方案场景特征建议方案原因少量字符串、短文本String.contains或indexOf代码简洁性能足够需要判断前缀/后缀startsWith/endsWith语义清晰局部匹配开销更小忽略大小写匹配toLowerCase()或regionMatches(true,...)regionMatches不产生新对象在循环中更友好正则规则匹配如手机号、邮箱Pattern预编译 matcher.find()避免反复编译正则效率高大量文本、多关键词匹配Aho-Corasick 或前缀树线性扫描一次可匹配所有模式串判断两个字符串“包含但忽略顺序”排序后比较或使用Set先结构化再比较思路更清晰实际项目中contains和indexOf选择通常是可读性优先。contains对团队里经验较少的成员更友好。而indexOf则在需要知道位置时才有必要出场。这里还有一种常见的技术讨论判断字符串是否包含某个子串能不能用StringUtils.isNotBlank(text) text.matches(.*关键词.*)。这种做法虽然能实现但性价比极低——正则做包含匹配是杀鸡用牛刀性能远不如contains还容易写出错误的通配符。我自己在项目里有一条不成文的规定能不用正则就不用正则除非真的有格式校验需求。2.4 字符串不可变与常量池对内存的影响很多人写包含判断时会顺手把目标字符串写成String keyword new String(关键词)这是一种负面的习惯。Java 字符串是不可变的new String(关键词)除了浪费一次对象创建没有其他意义。而直接写关键词会被放入字符串常量池多个地方的判断可复用同一对象内存占用更小。更隐蔽的问题是拼接字符串时的内存占用。比如String log ; for (String line : lines) { if (line.contains(ERROR)) { log line; } }这种写法每次循环都会创建新的 String 对象在日志量大的场景下会带来明显的 GC 压力。正确的做法是使用StringBuilder或StringJoiner。包含判断本身没错但连着字符串拼接一起写就容易把性能带偏。我在排查线上问题时就发现过因为这类代码导致 GC 频繁的服务后来改成StringBuilder之后GC 立刻恢复正常。判断逻辑的位置变了性能表现也会有差别。3. 空指针安全与边界情况处理字符串处理类的代码里最容易出事的往往不是算法而是“空值”和“边界”。一个contains方法在执行时如果遇到null调用方会直接抛出NullPointerException。这个错误本身不可怕可怕的是它在生产环境出现的时机不可控。我在实际项目里就碰到过因为上游接口返回了一个null而下游代码没有判空结果整个定时任务挂掉的情况。3.1 null安全判断的三种写法假设我们要判断一个可能为null的字符串text是否包含子串keyword常用的写法有以下三种// 写法一手动判空 if (text ! null text.contains(keyword)) { // ... } // 写法二固定常量放前面调用方不为 null if (keyword ! null keyword.length() 0 text ! null text.contains(keyword)) { // ... } // 写法三使用 Java 8 风格的 Optional不推荐过度使用 Optional.ofNullable(text) .map(s - s.contains(keyword)) .orElse(false);第一种写法最简单也是我推荐优先使用的。第三种虽然看起来“函数式”但可读性不如第一种。还有一点要注意把子串写在前面是有讲究的。比如常量.equals(str)这种写法可以避免str为null时空指针。但在contains方法上不能直接套用因为调用方是常量时被检查的则是目标串。正确姿势是if (keyword ! null text ! null text.contains(keyword))JDK 8 以后还引入了一个很有用的方法String.isBlank()在 Java 11 中正式提供可以同时判断null和空字符串不过isBlank()只能判断字符串本身是不是空白并不能帮助你判断是否包含子串。所以在写工具方法时我一般会自己实现一个isNullOrEmpty或者使用 Apache Commons Lang 的StringUtils.containsIgnoreCase它内部已经做了 null 安全处理。3.2 空字符串与长度为0时的行为差异.contains()在 Java 中是返回true的。这是一个很微妙的行为。从数学角度理解空串是任何字符串的子串因此可以认为“空串包含空串”是成立的。但在业务逻辑里这容易造成困惑。比如你要判断一段文本里是否包含某个用户输入的标签如果用户输入了空串你直接text.contains(input)会拿到true可实际上这个“包含”并没有业务含义。所以建议在做包含判断之前先加上目标字符串的非空校验public boolean containsKeyword(String text, String keyword) { if (text null || keyword null || keyword.isEmpty()) { return false; } return text.contains(keyword); }keyword.isEmpty()加上之后行为就更符合直觉了。一个空的关键词不应该命中任何文本。这个小细节在参数校验场景下特别重要。我之前在一个搜索系统里就因为这个原因用户传空关键词时把全量的数据都过滤出来了后来加上了空值校验才修复。3.3 大小写、不可见字符与Unicode的坑对于英文场景忽略大小写的包含判断非常常见。朴素做法是text.toLowerCase().contains(keyword.toLowerCase())这种方式简单有效但存在两个问题。第一toLowerCase()和toUpperCase()因语言区域设置不同会有差异。比如土耳其语环境下的I和i就有特殊映射关系。如果你在分布式服务中使用了不同的默认Locale同一段代码在不同机器上可能出现不同的判断结果。稳妥做法是指定Locale.ROOT或Locale.ENGLISH。第二它会产生额外的字符串对象。如果text本身又长又多多次调用toLowerCase()会白白增加 GC 压力。不可见字符也是个容易忽略的坑。用户在输入文本时可能会夹带一些零宽空格\u200B、BOM 头\uFEFF、换行符等这些字符肉眼看不见但contains会严格比较。我遇到过因为复制文档中的内容带了 BOM结果数据库查询始终匹配不上的情况。排查了半天最后用调试器看字符串的 char 数组才发现多了一个不可见字符。因此在判断用户输入内容时第一步最好做一次“标准化清洗”String normalized text.replace(\uFEFF, ).trim();如果有换行符或制表符导致匹配失败也可以用normalized.replaceAll(\\s, )来统一空白。当然这会引入正则注意不要在大循环里频繁调用。3.4 正则表达式灾难性回溯用matches或find做包含匹配时正则表达式本身也可能写出性能炸弹。典型的例子String text aaaaaaaaaaaaaaaaaaaaaaaaaaaaab; Pattern pattern Pattern.compile(^(a)b$); boolean matched pattern.matcher(text).matches();当字符串很长且匹配最终失败时(a) 这类嵌套量词会导致灾难性回溯程序可能卡上数秒甚至更久。这在日志分析和用户输入校验中尤其致命——一个恶意构造的字符串就能让你的服务线程卡死。排查是否踩了回溯陷阱的方法很简单在匹配方法前后分别记录System.nanoTime()如果耗时和文本长度不成线性关系就要怀疑正则写得有问题。优化方式包括减少嵌套量词、使用原子组(?a)、使用懒惰量词或者干脆避免用正则而改用contains。4. 实际业务场景与实战技巧4.1 文件名后缀与路径前缀校验文件名、资源路径的判断是endsWith和startsWith的高频使用场景。我曾经在一个文件上传服务中需要限制上传格式起初用filename.endsWith(.jpg)实现后来发现有人上传了.jsp文件也通过了排查后意识到文件名和扩展名的大小写问题abc.JPG是以.JPG结尾而不是.jpg。简单的修复办法是把后缀统一转小写再做判断更稳妥的还是用正则或者枚举配置。我在这个项目里最后采用的方案是public boolean isAllowedImage(String filename) { if (filename null) { return false; } String lower filename.toLowerCase(Locale.ROOT); return lower.endsWith(.jpg) || lower.endsWith(.jpeg) || lower.endsWith(.png) || lower.endsWith(.gif); }路径前缀的判断也常被忽略。比如判断一个 URL 是否以/admin开头如果直接startsWith(/admin)会遇到/administrator也匹配的情况。这时应该把判断改成startsWith(/admin/) || /admin.equals(path)才能精确匹配目录层级。这类边界问题不能再靠肉眼“我觉得没问题”来糊弄了写单元测试时把所有奇怪路径一并覆盖。4.2 集合内字符串包含判断过滤与查找流式处理是 Java 8 以后的主流写法。比如从一个订单列表中筛选出所有备注中包含“加急”的记录ListOrder urgentOrders orders.stream() .filter(order - order.getRemark() ! null order.getRemark().contains(加急)) .collect(Collectors.toList());这段代码里我保留了remark ! null的判断。别小看它很多线上 NPE 就是在 lambda 表达式里冒出来的。如果 Order 对象本身可能为null还得再补一层Objects::nonNull的过滤。流式写法虽然简洁但可读性是牺牲了一些的。在团队协作的项目中我建议把这样的判断逻辑提取成具名方法而不是在链式调用里写一堆 lambda。另外判断一个ListString中是否有元素包含某关键词常见错误是直接list.contains(keyword)这个方法只做完整相等比较不判断包含关系。正确做法是boolean anyMatch list.stream() .anyMatch(item - item ! null item.contains(keyword));如果是MapString, String则要注意 key 和 value 的判定维度。根据业务需求可能同时要判断 key 是否包含某关键字和 value 是否包含某关键字不能混淆。4.3 日志脱敏与关键词拦截日志脱敏是我见过包含判断最实用的场景之一。比如业务日志里含有手机号、身份证号、银行卡号需要判断是否命中敏感模式然后做部分替换。这时候用matches或者find就可以。我经常这样写private static final Pattern PHONE_PATTERN Pattern.compile(1[3-9]\\d{9}); public String maskPhone(String text) { if (text null) { return null; } Matcher matcher PHONE_PATTERN.matcher(text); return matcher.replaceAll(m - m.group().replaceAll(\\d(?\\d{4}), *)); }这种需求里如果只是想判断“是否包含手机号”那用find()就够了不要用matches()。判断完成之后再进行脱敏配合正则捕获组代码可以比较整洁地完成过滤。关键词拦截则是典型的“一对多”场景。比如一个评论系统里要拦截一批敏感词如果直接这样写boolean isSensitive sensitiveWords.stream() .anyMatch(word - content ! null content.contains(word));当敏感词数量较大的时候循环调用contains就是 O(m*n)。当敏感词列表有几千个、评论内容又长接口耗时可能迅速攀升。我之前优化过一个社区评论系统原始实现就是这种循环高峰期热点评论的审核耗时超过了 2 秒。后来改用了 Aho-Corasick 算法预先把敏感词构建成自动机过滤一条评论的耗时降到几十毫秒以内。如果嫌算法实现麻烦也可以借用现成的工具库比如Hutool的SensitiveUtil底层就是基于 DC 算法实现的敏感词树。它的思路值得借鉴先把关键词构建成树结构再对被检测文本做一次扫描。这就是典型的空间换时间。4.4 高效的大小写不敏感包含判断方案先说结论绝大多数场景下最稳妥且性能不错的大小写不敏感包含判断可以用StringUtils.containsIgnoreCaseApache Commons Lang 3或StringUtils.containsIgnoreCaseHutool。底层实现通常是逐字符Character.toLowerCase进行比较不创建大量中间字符串比toLowerCase().contains()更节省内存。如果你不想引入第三方库自己实现也很简单public static boolean containsIgnoreCase(String src, String target) { if (src null || target null) { return false; } int targetLength target.length(); if (targetLength 0) { return true; } for (int i 0; i src.length() - targetLength; i) { if (src.regionMatches(true, i, target, 0, targetLength)) { return true; } } return false; }这段实现利用了regionMatches的忽略大小写能力窗口滑动比对。虽然在大文本下效率不如 JDK 内部的优化indexOf但因为它没有toLowerCase创建的大对象在多次调用时内存表现反而更稳。对已经引入 Apache Commons Lang 的项目直接用它提供的工具方法即可。5. 面试常问与项目踩坑记录5.1 面试官会追问的几个点字符串包含判断是 Java 基础面试的高频题面试官一般不会只问“你会不会用 contains”而是层层深入我总结几个高频追问方向第一contains 和 indexOf 的区别是什么。回答要点contains内部就是indexOf(s.toString()) 0的封装indexOf返回位置索引可以用于后续的截取和定位。第二contains 方法的时间复杂度。在绝大多数 JDK 实现里是 O(n*m)不过 JDK 9 以后对单字符查找有优化对多字符子串仍是朴素匹配为主。第三如何高效判断一个长文本是否包含多个关键词。这里就能引出 Aho-Corasick 或前缀树的思路体现出面试者的工程视野。第四如何避免正则回溯带来的性能问题。能讲出具体案例的候选人通常比背八股文的更有竞争力。我还被问过这样一道题“如果一个字符串很大比如几十 MB你怎么判断它是否包含某个关键词”这时候就不是简单调用contains的问题了。我会先回答是否允许流式处理如果允许可以分段读取并用滚动窗口匹配避免一次性加载整个字符串到内存。Java 里可以用BufferedReader按行读配合自定义的跨行匹配逻辑或者用java.nio做内存映射文件。这类问题没有标准答案考察的就是你能否跳出 API 本身去思考资源约束。5.2 几个真实的线上事故说两个我印象深刻的线上事故。第一个和字符串常量与变量有关。业务方传参status可能为SUCCESS、success、Success原来的代码写的是if (SUCCESS.equalsIgnoreCase(status)) { // do something }看起来没问题但后来日志里频繁出现status为null的告警排查后才发现问题不是equalsIgnoreCase本身而是上游在下发数据时如果某字段无值会序列化成字符串null而不是null。这样一来null就匹配不上 SUCCESS逻辑没有任何报错但数据一直悄无声息地漏掉。从那以后我在做任何字符串状态判断时都会先做一层数据清洗和日志输出而不是盲目相信入参。第二个事故和endsWith有关。一个文件类型校验服务原来用url.endsWith(.mp4)来判断是否为视频地址。某一天 CDN 调整了 URL 规则所有地址都加上了签名参数比如https://example.com/video.mp4?signxxxx。endsWith(.mp4)直接失效线上视频全部无法播放。后来改成先解析 URL 获取路径部分再做后缀判断URL parsed new URL(url); String path parsed.getPath(); boolean isMp4 path.endsWith(.mp4);这类事故提醒我包含判断的方法名看似直白但它的匹配粒度、匹配范围要时刻结合真实数据的形态去确认。URL 里带 query、带锚点、带大小写变化都可能导致判断失效。5.3 工具类的封装建议项目开发中我建议把字符串包含判断封装成统一工具类而不是到处散写。一是为了统一 null 安全策略二是便于以后增加性能优化或者日志埋点。我这里给一个参考实现public final class StringMatchUtils { private StringMatchUtils() { } public static boolean containsIgnoreCase(String text, String keyword) { if (text null || keyword null) { return false; } int keywordLen keyword.length(); if (keywordLen 0) { return true; } for (int i 0; i text.length() - keywordLen; i) { if (text.regionMatches(true, i, keyword, 0, keywordLen)) { return true; } } return false; } public static boolean containsAny(String text, CollectionString keywords) { if (text null || keywords null || keywords.isEmpty()) { return false; } for (String keyword : keywords) { if (keyword ! null text.contains(keyword)) { return true; } } return false; } }注意类构造器设为private避免被实例化。工具方法都传入null安全判断。这样团队其他成员复用起来会比较省心。如果你的项目里已经有Spring也可以把工具类声明成一个Component并通过Autowired注入但要考虑工具类“无状态”的特性通常静态方法更合适。5.4 从 Java 8 到 Java 21字符串 API 的变化在 Java 8 时代String.contains和String.indexOf几乎是全部选择。Java 11 增加了String.isBlank()和String.repeat()等便捷方法但它们不直接服务包含判断。Java 15 正式推出的String.formatted()主要用于格式化。Java 17 开始在String内部存储上全面使用byte[]和压缩字符串优化contains的底层匹配对纯 ASCII 串更高效。Java 21 的虚拟线程解决了高并发线程开销问题但针对字符串匹配本身并没有新增颠覆性 API。因此在不同 JDK 版本之间迁移时字符串包含判断的代码基本可以做到无缝兼容。不过有一个细节要留意JDK 9 开始启用COMPACT_STRINGS如果字符串内容全部可以由 Latin-1 表示内部使用 1 字节/字符的存储结构如果包含中文等字符则自动升级为UTF16的 2 字节结构。这对indexOf的性能有一个微妙影响——纯 ASCII 匹配会比包含中文的匹配更快一点。这种性能差异在绝大多数系统里无关紧要但如果你想追求极致性能可以把匹配关键词和中国文本分开处理或者利用charset做预先归一化。5.5 关于“正确性”的最后一课做字符串包含判断写出正确代码是一回事写出“长期不出事”的代码是另一回事。我在实践中总结出几条心得第一凡是接收外部输入的地方必须先判空。不要假设上游会按文档传参。第二区分业务语义中的“包含”到底是子串包含、前缀匹配、后缀匹配还是“包含在集合中”不同语义对应不同方法。很多人把List.contains和String.contains混在一起聊其实完全是两回事。第三把敏感边界写进单元测试包括空串、null、大小写、特殊字符、超长字符串测试覆盖到了心里才踏实。第四正则表达式是最后的武器不要用它代替普通的子串匹配。第五当数据量真的大了不要犹豫直接上成熟的匹配算法或工具库自己造轮子前先评估维护成本。这些经验并非高深理论都是从一次次线上故障和低效代码排查中换来的。如果你在项目里遇到看似“玄学”的字符串匹配 bug不妨先从这三件事查起入参是否为 null、大小写是否一致、是否存在不可见字符。解决这三个问题大概率能恢复直觉。我在接手一些老系统的过程中最常做的优化工作之一就是把散落在各业务层的if (str.contains(xxx))统一收编到工具类再加上日志和测试。看起来只是重构其实对系统的鲁棒性提升非常明显。字符串包含判断作为 Java 开发里最基础的一环写好了不仅能省去后续排查的麻烦也能让团队的新人更快理解代码的业务逻辑。
返回列表