使用StringTokenizer复制String.split

5

鼓舞于这篇文章和我有数十亿个字符串需要解析的事实,我尝试修改我的代码来接受StringTokenizer而不是String[]

我与获得那种美味的x2性能提升之间唯一剩下的问题是,在进行解析时会遇到以下情况:

"dog,,cat".split(",")
//output: ["dog","","cat"]

StringTokenizer("dog,,cat")
// nextToken() = "dog"
// nextToken() = "cat"
我该如何使用StringTokenizer实现类似的结果?有没有更快的方法?
9个回答

阿里云服务器只需要99元/年,新老用户同享,点击查看详情
12

你只是在以逗号作为分词依据吗?如果是的话,我会编写自己的分词器-它可能会比更通用的StringTokenizer更加高效,因为后者可以查找多个标记,并且您可以使其按照您想要的方式运行。对于这样一个简单的用例,可以使用简单的实现。

如果有用的话,甚至可以实现Iterable<String>并获得增强型for循环支持和具有强类型的枚举而不是StringTokenizer提供的支持。如果需要帮助编写此类程序,请告诉我-这实际上应该不难。

此外,在跳离现有解决方案之前,我建议您尝试在实际数据上运行性能测试。你有没有想过你的执行时间有多少实际上被花费在 String.split 上?我知道你要解析很多字符串,但如果你之后要使用它们进行任何重要的操作,我会认为那将比分割更重要。


谢谢Jon,我手工解析了代码(使用了很多indexOf),现在速度提高了4倍! - Dani

10
在尝试使用StringTokenizer类后,我无法找到满足返回["dog", "", "cat"]要求的方法。此外,StringTokenizer类仅用于兼容性原因,鼓励使用String.split。从StringTokenizer的API规范中可以看出:

StringTokenizer是一个遗留类,为了兼容性而保留,虽然它的使用在新代码中被不推荐。建议任何寻求此功能的人使用Stringsplit方法或java.util.regex包。

由于问题在于String.split方法的表现较差,我们需要找到替代方案。 注意:我说"表现较差"是因为很难确定每种情况下StringTokenizer都优于String.split方法。此外,在许多情况下,除非字符串的分词确实是由适当的分析确定的应用程序的瓶颈,否则我认为这最终将成为过早的优化,如果有的话,我倾向于编写有意义且易于理解的代码,而不是进行优化。

现在,根据当前的需求,可能自己编写分词器并不太困难。

自己编写分词器!

下面是我编写的一个简单的分词器。需要注意的是,没有速度优化,也没有错误检查来防止超出字符串的末尾 - 这只是一个快速而简单的实现:

class MyTokenizer implements Iterable<String>, Iterator<String> {
  String delim = ",";
  String s;
  int curIndex = 0;
  int nextIndex = 0;
  boolean nextIsLastToken = false;

  public MyTokenizer(String s, String delim) {
    this.s = s;
    this.delim = delim;
  }

  public Iterator<String> iterator() {
    return this;
  }

  public boolean hasNext() {
    nextIndex = s.indexOf(delim, curIndex);

    if (nextIsLastToken)
      return false;

    if (nextIndex == -1)
      nextIsLastToken = true;

    return true;
  }

  public String next() {
    if (nextIndex == -1)
      nextIndex = s.length();

    String token = s.substring(curIndex, nextIndex);
    curIndex = nextIndex + 1;

    return token;
  }

  public void remove() {
    throw new UnsupportedOperationException();
  }
}

MyTokenizer将使用一个String作为分词器,另一个String作为分隔符,并使用String.indexOf方法来查找分隔符。通过String.substring方法产生令牌。

我认为通过在char[]级别而不是String级别上处理字符串可能会有一些性能改进。但我将把这个任务留给读者自行完成。

该类还实现了IterableIterator以利用Java 5中引入的for-each循环结构。 StringTokenizer是一个Enumerator,不支持for-each结构。

它是否更快?

为了找出这是否更快,我编写了一个程序来比较以下四种方法的速度:

  1. 使用 StringTokenizer
  2. 使用新的 MyTokenizer
  3. 使用 String.split
  4. 使用 Pattern.compile 预编译正则表达式。

在这四种方法中,字符串 "dog,,cat" 被分成了多个标记。虽然比较中包含了 StringTokenizer,但应注意它不会返回所需的结果 ["dog", "", "cat]

为了足够地显示出这些方法之间的差异,对标记进行了 100 万次重复操作。

用于简单基准测试的代码如下:

long st = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
  StringTokenizer t = new StringTokenizer("dog,,cat", ",");
  while (t.hasMoreTokens()) {
    t.nextToken();
  }
}
System.out.println(System.currentTimeMillis() - st);

st = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
  MyTokenizer mt = new MyTokenizer("dog,,cat", ",");
  for (String t : mt) {
  }
}
System.out.println(System.currentTimeMillis() - st);

st = System.currentTimeMillis();
for (int i = 0; i < 1e6; i++) {
  String[] tokens = "dog,,cat".split(",");
  for (String t : tokens) {
  }
}
System.out.println(System.currentTimeMillis() - st);

st = System.currentTimeMillis();
Pattern p = Pattern.compile(",");
for (int i = 0; i < 1e6; i++) {
  String[] tokens = p.split("dog,,cat");
  for (String t : tokens) {
  }
}
System.out.println(System.currentTimeMillis() - st);

结果

测试使用Java SE 6 (build 1.6.0_12-b04)运行,结果如下:

                   运行1   运行2   运行3   运行4   运行5
                   -----    -----    -----    -----    -----
StringTokenizer      172      188      187      172      172
MyTokenizer          234      234      235      234      235
String.split        1172     1156     1171     1172     1156
Pattern.compile      906      891      891      907      906

因此,从有限的测试和仅五次运行可以看出,StringTokenizer确实是最快的,但MyTokenizer排名第二。然后,String.split是最慢的,而预编译的正则表达式比split方法稍快。

与任何小基准测试一样,它可能不太代表实际情况,因此应该对结果持怀疑态度。


我认为这个方法应该是这样的:public String next() { if (nextIndex == -1) nextIndex = s.length();String token = s.substring(curIndex, nextIndex); curIndex = nextIndex + delim.length(); return token; } - Juan Carlos Blanco Martínez

4
注意:经过一些快速基准测试,Scanner的速度比String.split慢四倍左右。因此,请不要使用Scanner。 (我保留这篇文章以记录在这种情况下Scanner是一个糟糕的选择。(请勿因我建议使用Scanner而对我进行负面评价...)) 假设您正在使用Java 1.5或更高版本,请尝试使用实现了Iterator<String>的Scanner:Scanner
Scanner sc = new Scanner("dog,,cat");
sc.useDelimiter(",");
while (sc.hasNext()) {
    System.out.println(sc.next());
}

给出:

dog

cat

2
我相信Scanner在内部使用了正则表达式,因此问题的提出者可能无法获得他们正在寻找的性能提升。不过,值得一试,使用适当的基准测试 :) - Jon Skeet
2
一个快速的性能测试显示,StringTokenizer需要47毫秒,String.split需要625毫秒,Scanner需要2235毫秒。因此,我撤回我的建议。不要使用Scanner,它非常慢。 - Zarkonnen

2

根据需要进行分词的字符串类型,您可以基于String.indexOf()编写自己的分割器。您还可以创建一个多核解决方案,以进一步提高性能,因为字符串的分词是相互独立的。每个核心处理100个字符串的批次。使用String.split()或其他方法。


2
与其使用StringTokenizer,你可以尝试使用Apache Commons Lang中的StrTokenizer类。我引用一下它的描述: 这个类可以将一个字符串分割成许多较小的字符串。它的目标是完成类似于StringTokenizer的工作,但它提供了更多的控制和灵活性,包括实现ListIterator接口。空的标记可以被删除或返回为空。 我认为这听起来是你所需要的东西,你觉得呢?

1
你可以这样做。虽然不是完美的,但可能对你有用。
public static List<String> find(String test, char c) {
    List<String> list = new Vector<String>();
    start;
    int i=0;
    while (i<=test.length()) {
        int start = i;
        while (i<test.length() && test.charAt(i)!=c) {
            i++;
        }
        list.add(test.substring(start, i));
        i++;
    }
    return list;
}
如果可能的话,您可以省略列表并直接对子字符串进行操作:
public static void split(String test, char c) {
    int i=0;
    while (i<=test.length()) {
        int start = i;
        while (i<test.length() && test.charAt(i)!=c) {
            i++;
        }
        String s = test.substring(start,i);
         // do something with the string here
        i++;
    }
}
在我的系统上,最后一种方法比StringTokenizer解决方案更快,但您可能希望测试它在您的系统上的表现。当然,您可以通过省略第二个while循环的{}来缩短此方法,并且您可以使用for循环代替外部while循环并将最后的i++包含在其中,但我没有这样做,因为我认为那是不好的风格。

0

嗯,你能做的最快的事情就是手动遍历字符串,例如

List<String> split(String s) {
        List<String> out= new ArrayList<String>();
           int idx = 0;
           int next = 0;
        while ( (next = s.indexOf( ',', idx )) > -1 ) {
            out.add( s.substring( idx, next ) );
            idx = next + 1;
        }
        if ( idx < s.length() ) {
            out.add( s.substring( idx ) );
        }
               return out;
    }

这个(非正式测试)看起来比split快两倍。然而,这种迭代方式有点危险,例如它会在转义逗号上中断,如果您最终需要处理它(因为您的十亿个字符串列表有3个转义逗号),那么到时候您可能会失去一些速度优势。

最终,这可能不值得麻烦。


0
我会推荐使用Google的Guava Splitter。 我将其与coobird测试进行了比较,并得到以下结果:

StringTokenizer 104
Google Guava Splitter 142
String.split 446
正则表达式 299


-1
如果您的输入是有结构的,您可以看一下JavaCC编译器。它会生成一个Java类来读取您的输入。它会像这样:
TOKEN { <CAT: "cat"> , <DOG:"gog"> }

input: (cat() | dog())*


cat: <CAT>
   {
   animals.add(new Animal("Cat"));
   }

dog: <DOG>
   {
   animals.add(new Animal("Dog"));
   }

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