5

受此鼓舞,以及我有数十亿个字符串要解析的事实,我尝试修改我的代码以接受StringTokenizer而不是String[]

在我和获得美味的 x2 性能提升之间唯一剩下的就是当你在做的时候

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

StringTokenizer("dog,,cat")
// nextToken() = "dog"
// nextToken() = "cat"

如何使用 StringTokenizer 获得类似的结果?有没有更快的方法来做到这一点?

4

9 回答 9

12

您实际上只是在逗号上进行标记吗?如果是这样,我会编写自己的标记器 - 它最终可能比可以查找多个标记的更通用的 StringTokenizer 更有效,并且您可以让它按照您的喜好进行操作。对于这样一个简单的用例,它可以是一个简单的实现。

如果它有用,您甚至可以Iterable<String>使用强类型实现并获得增强的循环支持,而Enumeration不是StringTokenizer. 让我知道您是否需要任何帮助来编写这样一个野兽 - 这真的不应该太难。

此外,在与现有解决方案相差太远之前,我会尝试对您的实际数据进行性能测试。您知道实际花费了多少执行时间String.split吗?我知道你有很多字符串要解析,但是如果你之后对它们做任何重要的事情,我希望这比拆分更重要。

于 2009-06-12T13:11:11.457 回答
10

在修补StringTokenizer类之后,我找不到满足返回要求的方法["dog", "", "cat"]

此外,StringTokenizer仅出于兼容性原因才保留该类,并String.split鼓励使用 。来自 API 规范StringTokenizer

StringTokenizer是一个遗留类,出于兼容性原因保留,但不鼓励在新代码中使用它。建议任何寻求此功能的人使用split方法Stringjava.util.regex 包来代替。

由于问题是该String.split方法据称性能不佳,因此我们需要找到替代方法。

注意:我说的是“据说性能很差”,因为很难确定每个用例都会导致StringTokenizer优于该String.split方法。此外,在许多情况下,除非字符串的标记化确实是通过适当的分析确定的应用程序的瓶颈,否则我觉得它最终会成为过早的优化,如果有的话。在进行优化之前,我倾向于说编写有意义且易于理解的代码。

现在,从目前的需求来看,滚动我们自己的标记器可能不会太难。

推出我们自己的 tokenzier!

以下是我写的一个简单的分词器。我应该注意到没有速度优化,也没有错误检查来防止超过字符串的末尾——这是一个快速而肮脏的实现:

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采用 aString来标记化和 aString作为分隔符,并使用该String.indexOf方法执行对分隔符的搜索。令牌由该String.substring方法生成。

我怀疑通过在char[]级别而不是级别上处理字符串可能会有一些性能改进String。但我会把它作为练习留给读者。

该类还实现了IterableandIterator为了利用for-eachJava 5 中引入的循环构造。StringTokenizeris an 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(内部版本 1.6.0_12-b04)运行,结果如下:

                   运行 1 运行 2 运行 3 运行 4 运行 5
                   ----- ----- ----- ----- -----
字符串标记器 172 188 187 172 172
MyTokenizer 234 234 235 234 235
字符串拆分 1172 1156 1171 1172 1156
Pattern.compile 906 891 891 907 906

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

与任何小基准一样,它可能不是很能代表现实生活中的条件,因此应该使用一粒(或一堆)盐来获取结果。

于 2009-06-12T14:13:15.003 回答
4

注意:在完成了一些快速基准测试后,Scanner 的速度比 String.split 慢四倍。因此,请勿使用扫描仪。

(我留下这个帖子是为了记录在这种情况下 Scanner 是一个坏主意的事实。(阅读为:请不要因为我建议 Scanner 而对我投反对票......))

假设您使用的是 Java 1.5 或更高版本,请尝试实现的ScannerIterator<String> ,因为它发生了:

Scanner sc = new Scanner("dog,,cat");
sc.useDelimiter(",");
while (sc.hasNext()) {
    System.out.println(sc.next());
}

给出:

dog

cat
于 2009-06-12T13:24:38.367 回答
2

根据您需要标记的字符串类型,您可以基于 String.indexOf() 编写自己的拆分器。您还可以创建一个多核解决方案来进一步提高性能,因为字符串的标记化是相互独立的。批量处理 - 让我们说 - 每个核心 100 个字符串。执行 String.split() 或其他方法。

于 2009-06-12T13:18:59.130 回答
2

您可以尝试使用 Apache Commons Lang 中的 StrTokenizer 类,而不是 StringTokenizer,我引用了该类:

此类可以将一个字符串拆分为许多较小的字符串。它旨在完成与 StringTokenizer 类似的工作,但它提供了更多的控制和灵活性,包括实现 ListIterator 接口。

空标记可能会被删除或返回为 null。

这听起来像你需要的,我想?

于 2009-06-12T13:24:38.210 回答
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;
}

如果可能的话,你可以省略 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++ 包含在其中,但我没有不要在这里这样做,因为我认为这种风格很糟糕。

于 2009-06-12T13:59:14.600 回答
0

我会推荐谷歌的 Guava Splitter
我将其与coobird测试进行了比较,得到了以下结果:

StringTokenizer 104
Google Guava 拆分器 142
String.split 446
正则表达式 299

于 2012-11-21T22:10:22.930 回答
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;
    }

这个(非正式测试)看起来是分裂速度的两倍。但是,以这种方式迭代有点危险,例如它会在转义逗号上中断,并且如果您最终需要在某个时候处理它(因为您的十亿字符串列表中有 3 个转义逗号)已经允许它,您可能最终会失去一些速度优势。

最终它可能不值得打扰。

于 2009-06-12T14:05:09.737 回答
-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"));
   }
于 2009-06-12T13:21:35.753 回答