字符串分割是几乎所有Java项目都会碰到的需求,比如解析CSV字段、拆分配置项、处理日志行。JDK提供了两种经典手段:早期JDK 1.0就有的StringTokenizer,以及JDK 1.4之后加入的String.split。两者看起来功能相似,实际在返回形式、底层实现和行为细节上差别不小,选错了不仅代码别扭,还可能埋下性能或正确性的隐患。

一、返回形式与使用方式的根本差异
String.split返回的是一个String[]数组,它属于集合体系,可以直接配合for-each循环、Stream API使用,代码风格更现代。而StringTokenizer实现了Enumeration接口,通过hasMoreTokens和nextToken进行游标式的逐个遍历,写法上是典型的迭代器风格。
String data = "name,age,city";
// split 返回数组,适合现代写法
String[] fields = data.split(",");
for (String f : fields) {
System.out.println(f);
}
// StringTokenizer 游标式遍历
StringTokenizer st = new StringTokenizer(data, ",");
while (st.hasMoreTokens()) {
System.out.println(st.nextToken());
}
这个差异看似只是语法层面,但它决定了后续的处理方式。数组可以随时拿到长度、随机访问某个元素,还能直接转成List做流式处理;而StringTokenizer没有size方法,一旦开始遍历,中途想回退或者按索引取值都很麻烦。对于需要二次加工的业务逻辑,数组形式明显更灵活。
另外要注意,StringTokenizer的构造函数有两种形态:两参数版本只传分隔符,三参数版本可以传一个布尔值returnDelims,控制是否把分隔符本身也作为一个token返回。split则没有这种开关,它永远只返回被分隔的内容。
二、正则支持与行为细节的差别
split的方法签名是split(String regex),它接收的是正则表达式。这意味着你可以用一个模式灵活地分割,比如按连续的任意空白字符分割:
String line = "hello world java";
// 正则 \s+ 匹配连续空白,一次搞定
String[] words = line.split("\\s+");
// 结果: ["hello", "world", "java"]
// StringTokenizer 传 " " 时多个空格会被自动合并处理
StringTokenizer st = new StringTokenizer(line, " ");
// 也能得到三个词,但它不懂正则
但正则支持是把双刃剑。如果分隔符本身就是正则里的特殊字符,比如点号、竖线、问号,直接传给split会得到错误结果,必须转义。StringTokenizer的分隔符则是纯字符匹配,不存在这个问题:
String ip = "192.168.1.100";
// 错误:点号在正则中匹配任意字符
// ip.split(".");
// 正确:转义后使用
String[] parts = ip.split("\\.");
// StringTokenizer 无需转义
StringTokenizer st = new StringTokenizer(ip, ".");
还有一个容易被忽视的行为差异:对连续分隔符的处理。split会把连续分隔符之间的空字符串也保留下来,比如"a,,b".split(",")得到的是长度为3的数组,中间是空串;而StringTokenizer默认会直接跳过连续分隔符,不会产生空token。解析CSV这类允许空字段的场景时,这个差别直接影响数据正确性,用StringTokenizer会丢失空字段的位置信息。
此外,split还有一个split(String regex, int limit)的重载版本。limit为正数时最多分割出该数量的元素,后面内容不再切分;limit为0是默认行为,会去掉末尾的空字符串;limit为负数则保留末尾空串。这些精细控制StringTokenizer都不具备。
String s = "a,b,c,,,";
s.split(","); // [a, b, c] 末尾空串被去除
s.split(",", -1); // [a, b, c, , , ,] 保留全部
s.split(",", 2); // [a, b,c,,] 只分割一次
三、性能表现与选型建议
性能方面,StringTokenizer确实更快。它的实现就是简单的字符扫描,不涉及正则引擎,单次分割的耗时通常只有split的几分之一。split内部会调用Pattern.compile,不过JDK做了优化:当分隔符是单个非正则特殊字符(比如单个逗号)时,会走一条快速路径,绕过正则直接做字符匹配,性能差距会明显缩小。
但要注意,JDK 8之前的split每次调用都可能触发正则编译,在循环里频繁分割长字符串时开销会被放大。如果代码运行在高频路径上,比如每秒解析数十万条日志行,这种差距就值得认真对待。一个折中做法是显式预编译Pattern并复用:
// 预编译正则,避免重复编译开销
private static final Pattern COMMA = Pattern.compile(",");
public static String[] fastSplit(String input) {
return COMMA.split(input);
}
官方的态度也很明确:StringTokenizer的文档中明确说明它是一个遗留类,出于兼容性原因保留,新代码不推荐使用,建议用split或java.util.regex包代替。所以选型可以遵循这样的原则:绝大多数业务代码直接用split,语义清晰、功能完整、和集合体系无缝衔接;只有在极致性能要求且分隔规则简单的内部工具代码里,才考虑StringTokenizer,同时要确认空字段跳过的行为不会造成数据问题。
四、常见坑点与总结
实际使用中有几个坑需要留意。第一,split的参数是正则,特殊字符必须转义,这是最常见的事故来源,尤其是按点号分割IP、按竖线分割编号的场景。第二,split默认会丢弃末尾空串,做定长字段解析时最好用负数limit,否则数组长度可能比预期短,按索引取值就会越界。第三,StringTokenizer传入的分隔符字符串是字符集合而不是字符串整体,传多个字符表示任意一个都可作为分隔符,不要误以为是按整个子串分割。
总结一下:split是面向现代Java的正则分割方案,功能完整、表达力强、与集合生态兼容,是默认选择;StringTokenizer实现简单、速度快,但属于遗留API,行为细节与直觉有偏差,只适合特定的性能敏感场景。理解了它们在返回形式、正则支持、空字段处理和性能上的差异之后,根据实际需求选择也就不再纠结了。
StringTokenizerString.split字符串分割修改时间:2026-09-16 13:39:38