扫描器 vs. 分词器 vs. 字符串分割,它们在IT技术中的应用是什么?

168

我刚学习了Java的Scanner类,现在想知道它与StringTokenizer和String.Split有什么不同/竞争关系。我知道StringTokenizer和String.Split只能用于字符串,那么为什么我要使用Scanner来处理字符串呢?Scanner是否只是专门用于拆分字符串的一站式工具?

10个回答

249

它们本质上是不同场景下的工具。

  • Scanner 适用于需要解析字符串并提取不同类型数据的情况。它非常灵活,但可以说在仅需获取由特定表达式分隔的字符串数组时,其API相对较为繁琐。
  • String.split()Pattern.split() 提供了一种轻松的语法来执行后者,但本质上它们只能做到这点。如果您想要解析生成的字符串,或根据特定的标记在中途更改分隔符,它们都无法帮助您实现。
  • StringTokenizerString.split() 更加受限制,使用起来也有点棘手。它基本上是设计用于提取由固定子字符串分隔的标记。正因为这种限制,它比String.split()快约两倍。(参见我的String.split()StringTokenizer性能对比》)它还早于正则表达式API,而String.split()是其中的一部分。

从时间上可以看出,String.split() 在典型的机器上仍然可以在几毫秒内对数千个字符串进行标记化。此外,它优于StringTokenizer的地方在于,它将输出作为字符串数组返回,这通常是您想要的结果。使用由StringTokenizer提供的枚举类型,在大多数情况下都太过繁琐。从这个角度来看,StringTokenizer现在有点浪费空间,你也可以直接使用String.split()


8
看看 Scanner 对你在 String.Split 和 StringTokenizer 上运行的相同测试的结果也会很有趣。 - Dave
2
给我回答了另一个问题:“为什么在Java API注释中不建议使用StringTokenizer?”从这段文字中可以看出,答案似乎是“因为String.split()足够快”。 - Legs
2
那么StringTokenizer现在基本上已经被弃用了吗? - Steve the Maker
StringTokenizer 被认为是过时的,但我仍然会偶尔使用它来解析简单的内容,因为对于我所做的解析类型来说,它是最容易的。如果它们最终完全放弃它,我将不得不回头重写代码,但它已经被弃用了很久,似乎并不会消失。 :-) - Brian Knoblauch
4
我了解这是对一个旧问题的回答,但如果我需要将一个巨大的文本流即时分割成标记,那么 StringTokenizer 是否仍然是最佳选择,因为 String.split() 会简单地耗尽内存? - Sergei Tachenov
显示剩余5条评论

58
让我们首先淘汰StringTokenizer。它已经过时,甚至不支持正则表达式。它的文档说明如下:

StringTokenizer是一个保留的遗留类,出于兼容性原因而保留,尽管在新代码中不建议使用它。建议寻求此功能的任何人都使用Stringsplit方法或java.util.regex包。

所以我们立即抛弃它。这样就只剩下split()Scanner了。它们之间有什么区别呢?
首先,split()只是返回一个数组,这使得使用foreach循环变得容易:
for (String token : input.split("\\s+") { ... }

Scanner 更像是一个流:

while (myScanner.hasNext()) {
    String token = myScanner.next();
    ...
}

或者

while (myScanner.hasNextDouble()) {
    double token = myScanner.nextDouble();
    ...
}

(它有一个相当大的API, 所以不要认为它总是限制于这样简单的事情。)

这种流式接口在解析简单文本文件或控制台输入时非常有用,当您没有(或无法获取)所有输入时开始解析。

就我个人而言,我唯一记得使用Scanner的时间是在学校项目中,当我需要从命令行获取用户输入时。它使得这种操作变得容易。但是,如果我有一个要分割的String,几乎毫无疑问会选择使用split()


24
StringTokenizer比String.split()快2倍。如果您不需要使用正则表达式,就不要使用它! - Alex Worden
我刚刚使用了Scanner来检测给定String中的换行符。由于换行符可能因平台而异(请查看Pattern的javadoc!)并且输入字符串不能保证符合System.lineSeparator(),所以我发现Scanner更适合,因为它已经知道在调用nextLine()时要查找什么换行符。对于String.split,我将不得不提供正确的正则表达式模式来检测行分隔符,但我没有发现任何标准位置存储它(我能做的最好的就是从Scanner类的源代码中复制它)。 - ADTC

9

StringTokenizer一直存在。它是所有方法中最快的,但类似于枚举的风格可能不像其他方法那样优雅。

split在JDK 1.4上出现。虽然比tokenizer慢,但更易于使用,因为它可以从String类调用。

Scanner在JDK 1.5上出现。它是最灵活的,并填补了Java API长期存在的空白,支持与著名的Cs scanf函数系列等效的功能。


7

Split虽然比Scanner慢,但并不像Scanner那么慢。StringTokenizer比split要快。然而,我发现通过牺牲一些灵活性来获得速度提升是可以使速度快出两倍的,这一点在JFastParser中实现了。https://github.com/hughperkins/jfastparser

在包含一百万个double的字符串上进行测试:

Scanner: 10642 ms
Split: 715 ms
StringTokenizer: 544ms
JFastParser: 290ms

一些 Javadoc 会很好,如果你想解析除数字数据以外的其他内容怎么办呢? - NickJ
好吧,它是为速度而设计的,而不是美观。它非常简单,只有几行代码,所以如果您想要添加一些文本解析选项,那么可以这样做。 - Hugh Perkins

6
如果您有一个要进行分词的字符串对象,请使用String的split方法,而不是StringTokenizer。如果您正在解析来自程序外部的文本数据,例如从文件或用户处获取,那么Scanner非常有用。

5
就这样,没有辩解,没有理由吗? - jan.supol

4

String.split似乎比StringTokenizer慢得多。 split的唯一优点是您可以获得标记的数组。 此外,您可以在split中使用任何正则表达式。 org.apache.commons.lang.StringUtils具有split方法,其运行速度比两个分别快得多,即StringTokenizer或String.split。 但是,所有三者的CPU利用率几乎相同。 因此,我们还需要一种CPU占用较少的方法,但我仍然找不到。


3
这个答案略微荒谬。你说你正在寻找一些更快但“CPU消耗较少”的东西。任何程序都是由CPU执行的。如果一个程序没有完全利用你的CPU,那么它肯定在等待其他东西,比如I/O。这不应该成为讨论字符串分词时的问题,除非你正在进行直接磁盘访问(我们在这里显然没有这样做)。 - Jolta

4
我最近做了一些关于String.split()在高性能敏感情况下表现不佳的实验。你可能会发现这很有用。
要点是,String.split()每次都会编译一个正则表达式模式,因此可能会减慢程序运行速度,与使用预编译的Pattern对象并直接对其进行操作相比。 Java的String.split()和replace()的隐藏弊端

4
实际上,String.split() 并不总是编译模式。查看1.7版本的Java源代码,你会发现如果模式是单个字符而且没有被转义,它将在不使用正则表达式的情况下分割字符串,所以速度应该很快。 - Krzysztof Krasoń

2

对于默认情况,我建议使用Pattern.split(),但如果您需要最大的性能(特别是在Android上,我测试过的所有解决方案都相当慢),并且只需要按单个字符拆分,我现在使用自己的方法:

public static ArrayList<String> splitBySingleChar(final char[] s,
        final char splitChar) {
    final ArrayList<String> result = new ArrayList<String>();
    final int length = s.length;
    int offset = 0;
    int count = 0;
    for (int i = 0; i < length; i++) {
        if (s[i] == splitChar) {
            if (count > 0) {
                result.add(new String(s, offset, count));
            }
            offset = i + 1;
            count = 0;
        } else {
            count++;
        }
    }
    if (count > 0) {
        result.add(new String(s, offset, count));
    }
    return result;
}

使用 "abc".toCharArray() 方法可以获得字符串的字符数组。例如:
String s = "     a bb   ccc  dddd eeeee  ffffff    ggggggg ";
ArrayList<String> result = splitBySingleChar(s.toCharArray(), ' ');

2

一个重要的区别是String.split()和Scanner都可以产生空字符串,但StringTokenizer从不这样做。

例如:

String str = "ab cd  ef";

StringTokenizer st = new StringTokenizer(str, " ");
for (int i = 0; st.hasMoreTokens(); i++) System.out.println("#" + i + ": " + st.nextToken());

String[] split = str.split(" ");
for (int i = 0; i < split.length; i++) System.out.println("#" + i + ": " + split[i]);

Scanner sc = new Scanner(str).useDelimiter(" ");
for (int i = 0; sc.hasNext(); i++) System.out.println("#" + i + ": " + sc.next());

输出:

//StringTokenizer
#0: ab
#1: cd
#2: ef
//String.split()
#0: ab
#1: cd
#2: 
#3: ef
//Scanner
#0: ab
#1: cd
#2: 
#3: ef

这是因为String.split()和Scanner.useDelimiter()的分隔符不仅仅是一个字符串,而是一个正则表达式。我们可以将上面例子中的分隔符“ ”替换为“ +”,使它们的行为类似于StringTokenizer。

-5

String.split() 的功能非常好,但它有自己的限制,例如如果您想根据单个或双重竖杠(|)符号拆分下面所示的字符串,则无法正常工作。在这种情况下,您可以使用 StringTokenizer。

ABC|IJK


13
实际上,您可以使用"ABC|IJK".split("\|")对您的示例进行拆分。 - Tomo
"ABC||DEF||".split("\|") 实际上并不适用,因为它会忽略结尾的两个空值,导致解析变得比应该更复杂。 - Armand

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接