优先用 String.indexOf() 循环统计单字符,精准高效;正则适合复杂模式匹配;Unicode 代理对需用 codePoint 相关方法处理。

用 String.indexOf() 循环统计最稳
多数场景下,直接遍历比正则更直观、更可控,尤其当你要统计的只是单个字符(比如 'a')时。String.indexOf(char, fromIndex) 能精准定位每次出现位置,避免正则引擎开销和边界歧义。
常见错误是手写 for 循环时漏掉边界判断,或用 charAt(i) 配合 length() 但没处理空字符串——结果抛 StringIndexOutOfBoundsException。
- 从
index = 0开始调用indexOf(ch, index),返回-1就停 - 每次找到后把
index更新为foundIndex + 1(不是+2,别跳过重叠可能) - 空字符串或
null输入必须提前判,StringUtils.countMatches()这类工具类虽方便,但引入 Apache Commons 是额外依赖
int count = 0;
int index = 0;
while ((index = str.indexOf(ch, index)) != -1) {
count++;
index++; // 移动一位,继续找下一个
}正则 Pattern.compile().matcher().find() 适合模糊匹配
正则真正有用的地方不是数 'x' 出现几次,而是数“连续两个数字”“以大写字母开头的单词”这类模式。硬用 replaceAll("[^a]", "").length() 看似简洁,实则创建了新字符串,对长文本浪费内存,且无法区分大小写或 Unicode 字符。
典型翻车点:用 "a".split("a").length - 1 统计 'a'——遇到开头/结尾/连续多个 a 时结果错乱;或者忘记转义正则元字符,比如想统计 '.' 却写成 "\." 漏了双反斜杠,在 Java 字符串里得写成 "\\."。
立即学习“Java免费学习笔记(深入)”;
- 固定字符统计优先用循环,正则留着处理带逻辑的模式(如
"\d{2}") - 如果非要用正则,用
Pattern.compile(Pattern.quote(chStr)).matcher(str)安全转义 -
matcher.results().count()(Java 9+)比手动while(find())少写几行,但底层一样遍历
注意 char 和 int 的 Unicode 陷阱
Java 的 char 是 UTF-16 单元,不是 Unicode 码点。统计 emoji 或中文生僻字时,一个字符可能占两个 char(代理对),用 str.charAt(i) 或 indexOf(char) 会漏判或误判。
比如字符串 "??"(程序员 emoji),实际长度是 4(两个代理对),但只算作 1 个 Unicode 字符。这时候 indexOf('?') 根本找不到——因为 '?' 本身在 Java 中无法表示为单个 char。
- 需要精确按 Unicode 字符(code point)统计时,改用
str.codePointCount(0, str.length())配合str.offsetByCodePoints() - 或者用
str.codePoints().filter(cp -> cp == targetCodePoint).count()(Java 8+ 流式) - 普通英文字母、数字、ASCII 符号不受影响,该用
char还用char,别为兼容性过度复杂化
性能差异其实没你想的那么大
在万级字符以内,循环和正则的耗时差不到 1ms,别过早优化。真正拖慢的是反复创建对象:比如在循环里不断调用 str.substring(),或用 replaceAll() 生成一堆中间字符串。
容易被忽略的一点:如果统计操作高频发生(比如日志解析循环中),把 Pattern 编译结果缓存为 static final,避免重复编译;而 indexOf() 方案天生无状态,天然适合复用。
- 单次调用?选代码最直白的那个
- 高频调用?缓存
Pattern,或把循环逻辑封装成工具方法 - 不确定输入是否含代理对?先明确业务范围——日志、配置、用户昵称通常不用管,富文本或国际化字段才需 code point 级处理
事情说清了就结束










