使用StringTokenizer复制String.split

时间:2009-06-12 13:05:17

标签: java performance string split stringtokenizer

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

我和你之间唯一能够获得美味的x2性能提升的事实是,当你做的时候

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

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

如何使用StringTokenizer获得类似的结果?有更快的方法吗?

9 个答案:

答案 0 :(得分:12)

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

如果它有用,您甚至可以实施Iterable<String>并通过强类型而不是Enumeration提供的StringTokenizer支持获得针对循环增强的支持。如果你想要任何帮助编码这样的野兽,请告诉我 - 这真的不应该太难。

此外,我尝试在实际数据上运行性能测试,然后再跳过现有解决方案。您是否知道在String.split实际花费了多少执行时间?我知道你有很多字符串需要解析,但是如果你之后对它们做了很多重要的事情,我希望它比分裂要重要得多。

答案 1 :(得分:10)

在修改StringTokenizer课程后,我无法找到满足返回["dog", "", "cat"]的要求的方法。

此外,仅出于兼容性原因而留下StringTokenizer类,并且鼓励使用String.split。来自StringTokenizer的API规范:

  

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();
  }
}

MyTokenizerString标记为String并将String.indexOf作为分隔符,并使用String.substring方法执行分隔符搜索。标记由char[]方法生成。

我怀疑通过在String级而不是for-each级别处理字符串可以提高性能。但我会把它作为练习留给读者。

该类还实现了IterableIterator,以便利用Java 5中引入的StringTokenizer循环结构。Enumeratorfor-each },并且不支持StringTokenizer构造。

是否更快?

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

  1. 使用MyTokenizer
  2. 使用新的String.split
  3. 使用"dog,,cat"
  4. Pattern.compile使用预编译的正则表达式。
  5. 在四种方法中,字符串StringTokenizer被分隔为标记。虽然比较中包含["dog", "", "cat],但应注意它不会返回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); 的所需结果。

    重复令牌化总共100万次,以便有足够的时间注意方法的不同。

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

    StringTokenizer

    结果

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

                       Run 1    Run 2    Run 3    Run 4    Run 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
    

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

    与任何小基准一样,它可能不具备现实生活条件的代表性,因此结果应采用谷物(或土墩)盐。

答案 2 :(得分:4)

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

(我正在离开这篇文章记录扫描仪在这种情况下是一个坏主意的事实。(阅读:请不要因为建议使用扫描仪而向我投票,请...)

假设您使用的是Java 1.5或更高版本,请尝试执行Iterator<String>的{​​{3}}:

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

给出:

dog

cat

答案 3 :(得分:2)

根据您需要标记的字符串类型,您可以根据String.indexOf()编写自己的分割器。您还可以创建一个多核解决方案,以进一步提高性能,因为字符串的标记化是相互独立的。对批次的工作说 - 每个核心100个字符串。做String.split()或watever else。

答案 4 :(得分:2)

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

  

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

     

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

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

答案 5 :(得分: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解决方案更快,但您可能想测试它是如何工作的。 (当然你可以通过省略第二个{}来使这个方法更短一些,当然你可以使用for-loop而不是外部while循环并包括最后一个i ++,但我没有'这样做是因为我认为那种不好的风格。

答案 6 :(得分: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个转义的逗号)到你如果允许的话,你最终可能会失去一些速度优势。

最终它可能不值得打扰。

答案 7 :(得分:0)

我会推荐Google的番石榴Splitter 我将它与 coobird 测试进行了比较,得到了以下结果:

  

StringTokenizer 104
  Google Guava Splitter 142
  String.split 446
  regexp 299

答案 8 :(得分:-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"));
   }