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


原始标题:Counting Words in a String with Java