1. 概述
本文将介绍在 Java 中统计字符串中单词数量的多种实现方式。这个问题看似简单,但在实际开发中容易踩坑,比如对空格、标点、连字符等边界情况的处理。选择合适的方法不仅能提高准确性,还能避免后期维护的麻烦。
2. 使用 StringTokenizer
✅ 简单粗暴,适合基础场景
StringTokenizer
是 Java 中一个老牌工具类,适合快速拆分字符串并统计词数。
assertEquals(3, new StringTokenizer("three blind mice").countTokens());
assertEquals(4, new StringTokenizer("see\thow\tthey\trun").countTokens());
它默认能自动处理多种空白字符(如空格、制表符、换行等),开箱即用。
⚠️ 但遇到连字符或标点就容易翻车
例如:
assertEquals(7, new StringTokenizer("the farmer's wife--she was from Albuquerque").countTokens());
你会发现结果是 7
,因为默认分隔符只有空白字符,而 '
和 --
并不会被识别为分隔符,导致 wife--she
被当作一个词。
✅ 解决方案:自定义分隔符
我们可以传入额外的分隔符来修复这个问题:
assertEquals(7, new StringTokenizer("the farmer's wife--she was from Albuquerque", " -").countTokens());
这样 ' '
(空格)和 '-'
都被视为分隔符,结果就正确了。
✅ 适用于 CSV 等固定格式数据
assertEquals(10, new StringTokenizer("did,you,ever,see,such,a,sight,in,your,life", ",").countTokens());
📌 总结:StringTokenizer
用起来简单,但灵活性差,只适合分隔符明确且固定的场景。
3. 使用正则表达式
✅ 灵活强大,适合复杂文本处理
正则表达式能精准定义“什么是单词”。我们通常认为:
一个单词以字母开头,以空格或标点结束,但允许内部包含某些特殊字符(如撇号
'
)。
目标是:按空格和标点切分,但保留单词内部的撇号。
例如:
assertEquals(7, countWordsUsingRegex("the farmer's wife--she was from Albuquerque"));
assertEquals(9, countWordsUsingRegex("no&one#should%ever-write-like,this;but:well"));
✅ 核心正则:[\\pP\\s&&[^']]+
public static int countWordsUsingRegex(String arg) {
if (arg == null) {
return 0;
}
final String[] words = arg.split("[\\pP\\s&&[^']]+");
return words.length;
}
📌 正则解析:
\\pP
:匹配任何标点符号\\s
:匹配空白字符&&[^']
:取交集,排除撇号'
+
:匹配一个或多个连续字符
⚠️ 注意:split()
会忽略首尾空字符串,但如果中间有连续分隔符,会生成空字符串,不过不影响计数(因为空字符串不会被算作有效单词)。
📌 优势:一行代码搞定复杂分隔逻辑,适合处理用户输入、自然语言文本等非结构化数据。
🔗 更多正则知识可参考:Baeldung 正则表达式教程
4. 手动遍历 + String API
✅ 完全可控,性能最优,适合高频率调用场景
当正则性能不够或需要极致控制时,可以手动遍历字符,通过状态机判断单词边界。
核心思路
- 设置一个状态标志
flag
,初始为SEPARATOR
- 遇到合法字符(字母或允许的标点)且当前状态为分隔符时,视为新单词,计数 +1,状态切为
WORD
- 遇到非法字符,状态切回
SEPARATOR
特殊字符处理
我们需要定义哪些字符允许出现在单词内部,比如撇号 '
:
private static boolean isAllowedInWord(char charAt) {
return charAt == '\'' || Character.isLetter(charAt);
}
完整实现
private static final int SEPARATOR = 0;
private static final int WORD = 1;
public static int countWordsManually(String arg) {
if (arg == null) {
return 0;
}
int flag = SEPARATOR;
int count = 0;
int stringLength = arg.length();
int characterCounter = 0;
while (characterCounter < stringLength) {
if (isAllowedInWord(arg.charAt(characterCounter)) && flag == SEPARATOR) {
flag = WORD;
count++;
} else if (!isAllowedInWord(arg.charAt(characterCounter))) {
flag = SEPARATOR;
}
characterCounter++;
}
return count;
}
✅ 测试验证
assertEquals(9, countWordsManually("no&one#should%ever-write-like,this but well"));
assertEquals(6, countWordsManually("the farmer's wife--she was from Albuquerque"));
📌 优势:
- ✅ 性能高,无正则开销
- ✅ 可定制性强,比如支持连字符
-
、下划线_
等 - ✅ 易于调试,逻辑清晰
⚠️ 缺点:代码量稍多,需手动维护状态逻辑。
5. 总结
方法 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
StringTokenizer |
分隔符固定,如 CSV | 简单易用 | 灵活性差,难处理复杂标点 |
正则表达式 | 自然语言、用户输入 | 灵活强大,一行搞定 | 性能略低,正则难懂 |
手动遍历 | 高频调用、性能敏感 | 性能最优,完全可控 | 代码稍多,需手动维护 |
📌 建议:
- 日常开发推荐使用正则表达式,平衡了简洁与灵活性
- 对性能要求极高(如日志分析、搜索引擎)可选手动遍历
- 仅处理简单分隔场景可用
StringTokenizer
,但注意别被坑
🔧 所有示例代码已上传至 GitHub:https://github.com/baeldung/core-java-modules/tree/master/core-java-string-algorithms-2