ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java字符串包含判断:从contains到正则的完整实战指南

Java字符串包含判断:从contains到正则的完整实战指南 1. 内容整体设计与思路拆解1.1 为什么单独写“字符串包含判断”这个主题先聊个常见的场景你去面试面试官随手写一行str.contains(abc)问你这行代码背后发生了什么能不能换成别的方法实现各自的区别是什么。很多干了三五年的Java工程师平时用IDE自动补全用惯了真被问到这一层反而会卡壳。字符串包含判断看起来是基础中的基础但它牵扯出的东西一点都不基础编码问题、正则、性能、空指针、底层实现每一层都能挖出坑来。我平时在HoRain云上折腾微服务和中间件的时候日志分析、参数校验、路由匹配这些场景里字符串包含判断几乎天天都要用。很多线上问题最后排查下来根因就是当初写判断时选错了方法。所以这篇内容我不打算只列API而是把“判断字符串包含”这件事从需求场景到实现方案、从性能对比到踩坑实录完整拆一遍。无论你是刚学Java的新手还是写了几年想补充细节的老手都能从里面找到对你有价值的东西。1.2 包含判断的常见场景分类在动手写代码之前先想清楚一件事你写“包含判断”的最终目的是什么根据我接触过的项目大致分这么几类是否存在判断只要知道目标字符串里有没有某个子串不关心位置。比如判断用户输入的邮箱是否包含“”。位置定位不光要知道有没有还要拿到子串出现的位置。比如截取某个标记后面的内容。多次出现统计统计某个关键词在文本里出现几次。比如分析日志中某个错误码出现的次数。条件过滤与校验在批量数据里筛选符合条件的记录。比如从订单列表中找出所有包含“退款”字样的订单。安全性校验检查输入是否包含恶意字符、特殊符号或SQL关键字。这里通常需要结合白名单/黑名单机制来判断。不同场景对方法的选择要求完全不一样。比如只是“有没有”这种简单场景用contains就够了但如果需要忽略大小写或者要匹配模糊模式就得用matches或者正则。这次我们就把这些场景串起来逐个击破。2. 核心细节解析与实操要点2.1 Java字符串包含判断的常规手段Java里做字符串包含判断最常用的无非这几种方法方法作用返回值使用场景str.contains(CharSequence s)判断是否包含指定字符序列boolean简单包含判断str.indexOf(String s)返回子串首次出现的索引没有则返回-1int需要确定位置时str.lastIndexOf(String s)返回子串最后一次出现的索引int需要确定最后一次位置时str.startsWith(String prefix)判断是否以指定前缀开头boolean前缀匹配str.endsWith(String suffix)判断是否以指定后缀结尾boolean后缀匹配str.matches(String regex)判断整个字符串是否匹配正则表达式boolean正则匹配注意是整个字符串匹配Pattern.compile(regex).matcher(str).find()判断字符串是否包含符合正则的子串boolean复杂的子串匹配这里需要强调一个很容易踩的细节contains内部是调用indexOf(s) 0来实现的。也就是说contains本质上是indexOf的一个语法糖。从源码角度看String.contains在JDK 8中的实现是这样的public boolean contains(CharSequence s) { return indexOf(s.toString()) -1; }所以当你用contains时底层其实是在做一次字符串查找时间复杂度是 O(n*m)其中n是原字符串长度m是子串长度。JDK里的indexOf针对不同长度做了优化对于较短的子串使用了快速算法但本质上仍然是一次线性扫描。理解了这一点你就能明白为什么大量高频调用时字符串匹配会成为性能瓶颈。2.2 contains与indexOf的选择困惑我见过不少新手在写代码时纠结既然contains内部调用了indexOf那我是不是应该直接用indexOf省去一次方法调用说实话这种优化属于“伪优化”。现代JIT编译器会把这种简单的调用链内联掉实际性能差异小到可以忽略。真正值得关注的是你后续的业务逻辑如果你只需要知道“有没有”用contains可读性更好如果你还需要知道子串的位置则必须用indexOf。相比纠结那零点几纳秒的性能代码语义的清晰度重要得多。还有一个常见误用str.matches(.*abc.*)来判断包含。matches要求整个字符串匹配正则表达式如果你写matches(abc)那只有当整个字符串就是“abc”时才返回true而不是包含“abc”就返回true。很多人在这里栽过跟头。正确的做法是加上.*通配符或者干脆用find()String url https://horain.cloud/login; // 错误示范matches要求整个字符串匹配 System.out.println(url.matches(login)); // false // 正确示范用通配符包起来 System.out.println(url.matches(.*login.*)); // true // 更推荐用find来查找子串 Pattern pattern Pattern.compile(login); Matcher matcher pattern.matcher(url); System.out.println(matcher.find()); // true2.3 大小写不敏感的包含判断字符串包含判断还有一个高频需求忽略大小写。比如判断一段文本里是否包含“java”时用户可能输入的是“Java”“JAVA”“jAvA”希望都能命中。String里没有自带containsIgnoreCase方法所以通常有两种做法做法一统一转小写或大写再判断。String source HoRain Cloud Java 实战; String target java; boolean result source.toLowerCase().contains(target.toLowerCase());这种做法简单直接但有个副作用它会额外创建新的字符串对象如果source很大会消耗内存。还有一个隐藏问题某些语言如土耳其语的字母大小写转换规则和英语不一样toLowerCase()在特定Locale下可能产生意外结果。所以更稳妥的方式是指定Locale比如source.toLowerCase(Locale.ROOT)。做法二用正则表达式的Pattern.CASE_INSENSITIVE标志。Pattern pattern Pattern.compile(Pattern.quote(target), Pattern.CASE_INSENSITIVE); Matcher matcher pattern.matcher(source); boolean result matcher.find();这里用Pattern.quote(target)是为了把目标字符串转义成字面量防止target里含有正则特殊字符时出现误匹配。这个细节很多人不知道但非常关键。从性能角度看如果只是偶尔判断一次两种做法差别不大如果在一个循环里对大量数据做判断toLowerCase每次都要复制字符串会带来可观的GC压力。相比之下正则方式虽然初始化Pattern有一定开销但Pattern可以复用在循环场景下性能更好。我的经验是日常业务用toLowerCase足够性能敏感场景用预编译的Pattern。2.4 空指针与空字符串的防御写字符串判断最容易忽略的坑就是空指针。你传一个null给contains或者调用contains的对象本身是null都会直接抛出NullPointerException。String text null; text.contains(java); // NPE!而text.contains(null)也会触发NPE因为contains内部会把CharSequence转成字符串再调用indexOf在indexOf里会先判断参数是否为空如果是null则抛NPE。所以任何来自外部输入的字符串在调用包含判断之前一定要做空值校验。JDK 8之后提供了Optional但我觉得最直白的写法还是先判空if (text ! null text.contains(java)) { // do something }或者用Apache Commons Lang里的工具类StringUtils.contains(text, java);StringUtils.contains是null安全的传入null不会抛异常而是返回false。如果你的项目里已经引入了 Commons Lang3可以直接用它省得每次手写判空。但如果你不想引入额外依赖自己封装一个工具方法也行public static boolean containsIgnoreCase(String source, String target) { if (source null || target null) { return false; } return source.toLowerCase(Locale.ROOT).contains(target.toLowerCase(Locale.ROOT)); }2.5 使用正则时的特殊字符转义正则表达式是包含判断里的另一大坑。你写一个看起来正常的判断结果匹配结果完全不符合预期多半是目标字符串里含有正则元字符。举个真实例子判断URL里是否包含?id123这样的查询串如果直接写boolean result url.contains(?id123);这段代码用contains没问题因为contains做的是纯字面量匹配。但如果有人图省事改用正则boolean result url.matches(.*?id123.*);那就翻车了。?在正则里有特殊含义表示“前面的字符出现0次或1次”所以这个表达式匹配的是“id123”前面有个可选的任意字符和预期完全不符。正确写法是把特殊字符转义boolean result url.matches(.*\\?id123.*);或者更稳妥地用Pattern.quoteboolean result url.matches(.* Pattern.quote(?id123) .*);Pattern.quote会把所有字符当成普通字符处理自动完成转义。这是我在解析日志时最常用的技巧之一强烈推荐。3. 实操过程与核心环节实现3.1 搭建一个简单的测试环境为了把后面的例子完整跑起来我们先准备一个最基础的Java项目。这里我用Maven结构JDK版本用8以上都行因为我们的代码没有用到特别新的API。项目结构java-string-contains-demo ├── pom.xml └── src └── main └── java └── com └── horain └── demo ├── ContainsDemo.java └── StringCheckUtils.javapom.xml里只需要基础的依赖甚至没有外部依赖也能跑。为了演示方便我加一个JUnit做单元测试?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.horain/groupId artifactIdjava-string-contains-demo/artifactId version1.0-SNAPSHOT/version properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target /properties dependencies dependency groupIdorg.junit.jupiter/groupId artifactIdjunit-jupiter/artifactId version5.9.2/version scopetest/scope /dependency /dependencies /project3.2 实战案例日志关键词过滤先说一个我在HoRain云上排查问题时遇到的真实需求某天我们的网关服务在固定时间点出现大量超时为了定位是哪个上游接口拖慢了整体链路我需要从海量日志里筛出所有包含“Timeout”且包含特定接口路径“/api/order”的日志行。这个需求如果用直觉做法就是遍历日志文件每一行然后做两次contains。我先把基础版本写出来public class ContainsDemo { public static void main(String[] args) throws IOException { String logFilePath /var/log/gateway/access.log; BufferedReader reader new BufferedReader(new FileReader(logFilePath)); String line; int count 0; while ((line reader.readLine()) ! null) { if (line.contains(Timeout) line.contains(/api/order)) { count; System.out.println(line); } } reader.close(); System.out.println(匹配行数: count); } }这个写法能跑但有几个问题如果日志文件很大逐行读取并输出到控制台可能会拖慢速度应该把结果写入文件。判断逻辑直接写在main方法里不够通用不适合后续复用。如果“Timeout”大小写不固定这个判断会漏掉数据。改进方向把判断逻辑封装成一个工具方法并支持忽略大小写。于是有了StringCheckUtilspublic class StringCheckUtils { private StringCheckUtils() { } /** * 判断源字符串是否包含目标关键词忽略大小写 */ public static boolean containsIgnoreCase(String source, String target) { if (source null || target null) { return false; } return source.toLowerCase(Locale.ROOT).contains(target.toLowerCase(Locale.ROOT)); } /** * 判断源字符串是否同时包含多个关键词全部忽略大小写 */ public static boolean containsAllIgnoreCase(String source, String... targets) { if (source null || targets.length 0) { return false; } for (String target : targets) { if (!containsIgnoreCase(source, target)) { return false; } } return true; } }主程序改成public class LogFilter { public static void main(String[] args) throws IOException { Path logFile Paths.get(/var/log/gateway/access.log); ListString lines Files.readAllLines(logFile, StandardCharsets.UTF_8); ListString matched lines.stream() .filter(line - StringCheckUtils.containsAllIgnoreCase(line, Timeout, /api/order)) .collect(Collectors.toList()); Files.write(Paths.get(/tmp/matched.log), matched, StandardCharsets.UTF_8); System.out.println(匹配行数: matched.size()); } }这里用Files.readAllLines一次性读入内存如果日志文件是GB级别内存会爆所以只是演示场景。真实工作中我会用Stream逐行处理try (StreamString stream Files.lines(logFile, StandardCharsets.UTF_8)) { stream.filter(line - StringCheckUtils.containsAllIgnoreCase(line, Timeout, /api/order)) .forEach(System.out::println); }注意Files.lines返回的Stream用完后要关闭所以放在try-with-resources里。这是我实际排查问题时的标准操作。3.3 实战案例URL路径匹配与路由转发另一个高频场景是网关或者Web框架里的路由匹配。比如你写了一个简单的转发器要求是如果请求路径以/api开头且不包含/admin就把请求转发到后端服务否则拒绝。这个场景里的“包含”判断就涉及组合逻辑了public class RouteMatcher { private static final String API_PREFIX /api; private static final String ADMIN_KEYWORD /admin; public static boolean isLegalApiPath(String requestPath) { if (requestPath null || requestPath.isEmpty()) { return false; } if (requestPath.startsWith(API_PREFIX) !requestPath.contains(ADMIN_KEYWORD)) { return true; } return false; } public static void main(String[] args) { System.out.println(isLegalApiPath(/api/order/list)); // true System.out.println(isLegalApiPath(/api/admin/user/list)); // false System.out.println(isLegalApiPath(/public/order/list)); // false } }这个例子看起来简单但注意一个细节startsWith区分大小写。实际请求URL里的路径大小写可能不一致所以有时候需要先统一转小写再判断。另外contains在这个场景内没有使用正则性能极好适合高并发网关场景。如果把startsWith换成matches用正则匹配前缀反而会引入不必要的性能开销。所以先想清楚需求再选择最匹配的API。3.4 实战案例基于字符串包含的敏感词过滤敏感词过滤是很多内容社区和消息系统的刚需。实现方式有很多从最简单的一串contains到高效的Trie树字典树。我在这里演示一个基于contains的简单版本适合关键词数量不多的小型系统public class SensitiveWordFilter { private static final ListString SENSITIVE_WORDS Arrays.asList(垃圾, 骗子, 广告); public static String filter(String input) { if (input null || input.isEmpty()) { return input; } for (String word : SENSITIVE_WORDS) { input input.replace(word, ***); } return input; } public static void main(String[] args) { String content 这是一个广告别信骗子; System.out.println(filter(content)); // 这是一个***别信*** } }但这种方法在关键词数量多、文本很长的时候效率非常低因为每次都从头扫描字符串。如果你们系统对性能有要求建议参考AC自动机Aho-Corasick算法一次遍历匹配多个关键词。篇幅原因这里先不展开但你们要记住contains适合判断少量关键词不适合做高吞吐的敏感词检测。我实际在HoRain云上做消息过滤时最初就是用简单的contains先验证业务逻辑等关键词列表膨胀到几千个以后才切换到AC自动机。这也是一个迭代优化思路先用简单的方案跑通再根据监控数据去优化热点路径。3.5 实战案例判断字符串是否为数字包含判断延伸出来一个常见面试题如何判断一个字符串是否包含数字或者反过来如何判断一个字符串是否全部由数字组成如果是“全部由数字组成”最简单的做法是遍历每个字符判断是否在0到9之间public static boolean isNumeric(String str) { if (str null || str.isEmpty()) { return false; } for (char c : str.toCharArray()) { if (c 0 || c 9) { return false; } } return true; }还有正则写法public static boolean isNumeric(String str) { return str ! null str.matches(\\d); }注意\\d与\\d*的区别\\d*可以匹配空字符串所以如果传入空字符串会返回true这个要看业务能否接受。如果是“包含数字”只需要把判断改成public static boolean containsDigit(String str) { if (str null) { return false; } for (char c : str.toCharArray()) { if (Character.isDigit(c)) { return true; } } return false; }这其实就是一个“遍历字符 包含判断”的典型组合。理解了原理你就能灵活应对面试官的变形题。4. 常见问题与排查技巧实录4.1 为什么contains判断时灵时不灵——编码问题有次客户反馈某个接口校验“是否包含中文”时在本地测试正常部署到服务器上就失灵。最后排查发现服务器的默认字符集是GBK而代码里读文件用的是默认编码导致中文乱码自然匹配不上。Java里的String对象本身是Unicode编码理论上不受运行时环境字符集影响。但问题往往出在外部数据转换环节——读取文件、网络流、数据库连接时如果编码指定不一致就会产生乱码。最典型的BufferedReader reader new BufferedReader(new InputStreamReader( new FileInputStream(filePath)));这段代码没有指定字符集JVM会使用平台默认字符集。在Linux服务器上可能是UTF-8在Windows上可能是GBK结果就不同。正确写法BufferedReader reader new BufferedReader(new InputStreamReader( new FileInputStream(filePath), StandardCharsets.UTF_8));后来排查问题凡是字符串包含判断失败我都会先检查数据源头编码。特别是从数据库或消息队列拿到的数据如果中途经过转码很容易出现看不见的字符问题。像零宽空格、不间断空格这类特殊字符在Debug时看不出来但用contains判断就会意外返回false。我当时写了一个辅助方法去打印每个字符的Unicode码专门用于这种排查public static void printUnicode(String str) { for (int i 0; i str.length(); i) { System.out.printf(index%d, char%s, unicode%04x%n, i, str.charAt(i), (int) str.charAt(i)); } }通过看Unicode码很快就能发现混入了不可见字符。4.2 为什么用matches匹配包含总是返回false前面提到过matches是“整个字符串”匹配很多新手不理解。这里再举一个案例String s abc123; System.out.println(s.matches(\\d)); // false因为abc123整体不是纯数字 System.out.println(s.matches(.*\\d.*)); // true因为包含连续数字如果你在“包含数字”的需求里直接写s.matches(\\d)永远得到false。正确的包含正则写法是s.matches(.*\\d.*)注意.*\\d.*里\\d表示单个数字如果你写\\d同样也能匹配但语义上不如\\d简洁。对于“至少包含一个数字”用.*\\d.*足够了。很多人在正则里纠结于^和$但在Java的matches里^和$不是必须的因为该方法默认就是全匹配。如果你从别的语言如JavaScript转过来会习惯写/abc/.test(str)表示包含而在Java里要用Pattern.compile(abc).matcher(str).find()才能达到同样的效果。这是跨语言迁移时最容易踩的坑。4.3 为什么contains在循环中越来越慢——性能陷阱有一次我在处理一个包含2万条数据的List每一条都要做3次contains判断测试环境跑起来很流畅但数据量翻到10万后接口响应时间从50毫秒涨到800毫秒。分析发现contains本身不是瓶颈瓶颈在于我每次循环内部调用了toLowerCase来忽略大小写而源字符串有几百个字符等于每次都要复制一份大字符串GC压力剧增。优化方式分两步提前把源字符串转为小写一次循环里直接用转换后的结果判断。目标关键词全部预编译成正则Pattern。改写后的代码String lowerSource source.toLowerCase(Locale.ROOT); for (String item : itemList) { if (lowerSource.contains(item.toLowerCase(Locale.ROOT))) { // ... } }更好的是把item.toLowerCase的结果也缓存起来因为关键词集合往往比数据列表小得多缓存命中率很高。还有一个性能优化点是如果业务允许优先用String.indexOf配合StringBuilder处理。比如要做多个关键词的连续匹配contains每次都会从头开始扫描而如果你用indexOf加上搜索起始位置可以避免重复扫描已检查过的区域。这个优化在某些大文本场景下收益非常明显。4.4 常见问题速查表问题现象可能原因解决方案contains抛空指针调用对象或参数为null先判空或使用StringUtils.containscontains判断大小写敏感导致漏判业务需要忽略大小写统一转小写或使用Pattern.CASE_INSENSITIVEmatches判断包含总是falsematches是全匹配不是包含匹配使用.*目标.*或改用find()中文包含判断失败数据源编码与读取编码不一致统一使用UTF-8检查数据源头特殊字符?,.,*匹配异常正则元字符未转义使用Pattern.quote()循环调用大量contains性能差频繁转换字符串大小写或重复扫描预编译Pattern缓存小写结果判断结果意外为true混入了不可见字符如零宽空格使用Unicode码打印排查4.5 我的几个排查技巧再分享几个日常工作的习惯不一定写在文档里但很实用。第一先打印再判断。在怀疑字符串包含判断出错时先把源字符串和目标字符串都打印出来特别是length()我遇到过字符串里有尾随空格导致的判断失败肉眼根本看不出来一打印长度就露馅。第二用常量定义关键词。在工程里我会把需要做包含判断的关键词提取成常量避免魔法字符串散落各处。例如public static final String PAY_SUCCESS 支付成功;这样在修改关键词时只改一处不会出现改了判断条件忘了改提示语的情况。第三考虑把判断逻辑收口到工具类。一个项目里可能有几十个地方都做字符串包含判断如果各自用contains一旦要统一加日志、加判空、加缓存就要改几十处。收口到工具类之后所有调用方自动受益。这也是为什么我在文章里反复推荐自己封装StringCheckUtils的原因。5. 性能对比与方案选型建议5.1 不同实现方式的性能实测为了让大家有一个直观概念我写了一个简单的基准测试针对同一个文本内容用不同方式判断是否包含目标子串循环执行100万次记录耗时。测试环境JDK 8默认JVM参数CPU为4核。测试代码如下public class ContainsBenchmark { private static final String SOURCE HoRain云平台提供弹性计算、对象存储、负载均衡等多种云服务产品帮助企业和开发者快速上云。; public static void main(String[] args) { int times 1_000_000; // 方式1contains long start1 System.nanoTime(); for (int i 0; i times; i) { SOURCE.contains(云服务); } long end1 System.nanoTime(); System.out.println(contains耗时: (end1 - start1) / 1000 us); // 方式2indexOf long start2 System.nanoTime(); for (int i 0; i times; i) { SOURCE.indexOf(云服务) 0; } long end2 System.nanoTime(); System.out.println(indexOf耗时: (end2 - start2) / 1000 us); // 方式3正则matches每次编译 long start3 System.nanoTime(); for (int i 0; i times; i) { SOURCE.matches(.*云服务.*); } long end3 System.nanoTime(); System.out.println(matches耗时: (end3 - start3) / 1000 us); // 方式4预编译Pattern.find Pattern pattern Pattern.compile(云服务); long start4 System.nanoTime(); for (int i 0; i times; i) { pattern.matcher(SOURCE).find(); } long end4 System.nanoTime(); System.out.println(预编译find耗时: (end4 - start4) / 1000 us); } }在我机器上的一个典型输出单位微妙不是毫秒实现方式耗时微秒contains58,432indexOf55,217matches每次编译1,203,456预编译Pattern.find213,845这个数据说明几个问题contains和indexOf性能几乎一致所以不要为了“少一层调用”而放弃可读性。正则matches每次编译的开销巨大比contains慢20倍以上。在高频路径上务必避免每次调用都编译正则。即使预编译了正则find依然比contains慢3~4倍因为正则需要状态机匹配。所以简单字面量判断能用contains就不要用正则。5.2 方案选型口诀根据上面的数据和实际项目经验我总结了一个选型口诀字面量包含用contains可读性好性能足够。需要位置用indexOf/lastIndexOf别硬用contains猜位置。前后缀匹配用startsWith/endsWith语义清晰且快。忽略大小写先统一转为小写再用contains条件允许时缓存转换结果。正则匹配子串用预编译Patternfind()别用matches代替。多关键词高吞吐场景考虑 AC 自动机或 TST三叉搜索树别用大量contains叠加。5.3 什么情况下必须用正则再补充一下正则并不是洪水猛兽。下面这些场景只有正则才能搞定必须用模糊匹配比如匹配“包含至少一位数字 至少一个字母”的字符串。动态模式规则不是固定的字面量而是由用户配置的表达式。分组捕获在匹配的同时还要提取出子串的一部分比如从邮箱中提取用户名。这时候用Pattern.compileMatcher.find是唯一合理选择。只要注意预编译和复用Pattern性能瓶颈完全可控。6. 结合框架场景的扩展应用6.1 在Spring Boot接口参数校验中的应用在Spring Boot项目里经常需要对请求参数做包含判断。比如校验一个枚举字段是否合法或者判断一个查询参数是否包含非法字符。一种常见的做法是在Controller层写一堆if (param.contains(...))但更好的做法是用Spring Validation的注解或者自己写一个自定义校验注解。不过这里不讲那么复杂只说最基础用法RestController RequestMapping(/api) public class DemoController { private static final ListString BLOCKED_WORDS Arrays.asList(drop, truncate); PostMapping(/search) public Result search(RequestBody SearchRequest request) { String keyword request.getKeyword(); if (keyword ! null BLOCKED_WORDS.stream().anyMatch(keyword::contains)) { return Result.error(关键词包含非法内容); } // 正常业务逻辑 return Result.success(...); } }这段代码有个细节要注意keyword::contains是方法引用等价于word - keyword.contains(word)意思是判断关键词列表中的每个词是否被keyword包含。这里的方向和我们平常写的keyword.contains(xxx)是反的很多人写混淆了。如果判断错了方向过滤器就会失效。写完后最好用单元测试覆盖一下。在Spring里的拦截器或者过滤器我一般会用一个常量池来保存需要判断的关键词配合contains做前置过滤。实际项目里这种黑名单逻辑往往出现在网关层。比如HoRain云平台的API网关里就有一层请求参数白名单校验采用的就是类似思路。6.2 在MyBatis Plus动态SQL中的应用热搜词里出现了“mybatisplus根据java实体类生成创建表的sql语句”这个和字符串包含判断的关联点在哪儿其实就是生成SQL字符串时需要判断字段是否包含某个关键字比如判断字段名是否以某个前缀开头、是否包含下划线等。举个例子写一个工具类根据实体类生成建表SQL时常需要判断属性名是否包含TableField注解或者字段名里是否包含_。这时候contains和indexOf就派上用场了if (field.getName().contains(_)) { // 说明是下划线命名需要特殊处理 }更常见的是在代码生成器里根据字段名是否包含is、has等前缀来决定生成什么类型的列定义。这类需求本质上还是字符串包含判断的延伸。6.3 在日志链路追踪中的应用链路追踪里有一个典型场景从一条日志的traceId中提取业务标识。比如traceId格式是order_20231101_123456我们要判断它是否包含订单号前缀“order”同时要截取出后面的部分。这里我会用contains配合indexOfString traceId order_20231101_123456; if (traceId.contains(order_)) { int index traceId.indexOf(order_); String prefix traceId.substring(index, index order_.length()); String bizId traceId.substring(index order_.length()); System.out.println(bizId); // 20231101_123456 }虽然contains已经能判断出来了但真正拿子串时还是要indexOf。这也印证了我前面说的先想清楚需求再用合适的API别一开始就套正则。7. 关于“Java字符串包含判断”的延伸思考7.1 从包含判断到字符串算法的进阶“字符串包含”看起来简单但它背后连接着字符串匹配算法这个大坑。面试时如果被问到“如何在大文本中高效查找子串”你可以从朴素的逐个匹配讲到indexOf的优化再到KMP、Boyer-Moore、Rabin-Karp这些经典算法。JDK内部的indexOf实现对短模式长度小于等于7使用了暴力匹配对长模式则有一个优化先比较最后一个字符如果不匹配就快速跳过。这些优化虽然不像KMP那么理论化但在实际场景中效果非常显著。如果你对性能有极致的追求可以自己实现KMP但在绝大多数业务场景里JDK的默认实现已经足够好。7.2 字符串池与内存影响还有一点容易被忽视字符串包含判断常常会用到substring或toLowerCase这些操作在JDK 8及以前可能会产生内存问题。比如substring在JDK 7u6之前会共享底层char数组导致一个大字符串截取一个小子串却依然持有着大数组的引用造成内存泄漏。不过这个问题在现代JDK里已经修复了但如果你们还在维护老版本JDK读字符串相关代码时要格外当心。在高并发环境下大量的contains判断如果伴随toLowerCase会创建大量临时对象。用-XX:PrintGCDetails观察GC日志经常能看到年轻代频繁回收。针对这个场景我一般会做两件事一是减少无谓的字符串复制二是把不可变的关键词集合用List或Set静态初始化避免在方法调用里反复创建。7.3 语言对比为什么其他语言更“省事”很多从Python或JavaScript转Java的人会觉得Java字符串操作很繁琐。Python里abc in text一句话搞定JavaScript里abc.includes(a)也简单。Java的contains其实也不复杂只是类型系统更严格加上没有内置的忽略大小写方法让很多人初期不适应。不过Java的严谨也有好处。比如你明确区分了contains字面量包含和matches正则全匹配就不会像JavaScript里那样时不时因为includes和match的语义差异而出bug。API设计得不够“智能”反而逼着程序员想清楚自己到底要什么。7.4 如何把这些知识应用到面试中面试官问“Java字符串包含判断”时别只回答一个contains就结束。你可以展示自己的深度先讲contains与indexOf的关系内部实现。再讲大小写不敏感和空指针安全的处理方式。然后扩展到正则的matches和find区别举一个踩坑例子。最后聊性能对比提到预编译Pattern和AC自动机。这样的回答从API使用到源码原理再到性能优化层层递进面试官基本就能判断出你是一个有实战经验的工程师而不是只会背API的“调用仔”。这也是我写这篇内容的初衷把一个“小问题”讲出“大学问”让大家在项目里少踩坑在面试时多加分。8. 个人实操心得总结写了这么多最后说几句掏心窝子的话。字符串包含判断是Java里最不起眼的操作之一但恰恰是这种不起眼的地方最容易埋雷。我工作这么久踩过的最深的坑不是分布式一致性不是高并发缓存反而是这种几行代码就能写完的字符串判断。所以遇到看似简单的代码我也会多问自己一句输入会不会是null大小写有没有要求目标字符串里有没有正则特殊字符数据源的编码统一了吗我的习惯是写一个项目统一的字符串工具类把所有包含判断、判空、大小写转换都收口到里面。不要追求每个地方都写得“很精简”更不要因为JDK没有提供某个方法就硬用复杂替代。一个contains能解决的问题不要为了炫技而引入正则但该用正则的场景也不要因为性能担忧而绕弯路。工具方法多写几个分支多覆盖几个边界情况长远来看收益非常大。最后再分享一个小技巧在IDE里调试字符串包含判断时善用“Evaluate Expression”窗口。我经常在断点处直接输入source.contains(xxx)来快速验证判断结果比一遍遍打印日志高效得多。如果发现判断结果与预期不符就用source.codePointAt(index)检查每个字符的码点特别适合排查不可见字符捣乱的情况。以上便是我在Java字符串包含判断上的全部经验了希望对你有用。如果你在实际项目里也遇到过类似的隐藏坑或者有更好的实践方式欢迎一起探讨。
返回列表