2009-06-12 50 views
5

通过this鼓励,而事实上我有十亿串的解析,我想修改我的代码接受的StringTokenizer代替的String []复制String.split与StringTokenizer的

唯一剩下在我和获得美味的x2性能提升之间的事实是,当你在做

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

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

我怎样才能达到与StringTokenizer类似的结果?有更快的方法来做到这一点?

回答

12

你真的只用逗号进行标记吗?如果是这样,我会写我自己的标记器 - 它可能最终会比更通用的StringTokenizer更有效,它可以查找多个标记,并且可以使其行为符合您的需要。对于这样一个简单的用例,它可以是一个简单的实现。

如果它很有用,甚至可以实现Iterable<String>,并通过强类型获得增强型for循环支持,而不是StringTokenizer提供的Enumeration支持。让我知道,如果你想帮助编写这样一个野兽,那真的不应该太难。

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

+1

+1,我喜欢实施Iterable 的想法! – coobird 2009-06-12 13:13:51

+0

谢谢乔恩,我手工解析(使用大量的indexof),现在它快了x4! – Dani 2009-06-12 13:56:03

2

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

-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")); 
    } 
2

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

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

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

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

4

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

(我离开后直至记录的事实,扫描仪在这种情况下,一个坏主意(读作:不downvote我要提示扫描仪,请...))

假设您正在使用Java 1。5或更高版本,尝试Scanner,它实现Iterator<String>,因为它发生:

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

给出:

dog 

cat 
+2

我相信Scanner在内部使用正则表达式,所以OP可能无法获得他们正在寻找的性能提升。值得一试虽然,与一个合适的基准:) – 2009-06-12 13:31:26

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

如果可能的话,你可以ommit名单的事情,直接做一些事来的字符串:

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循环而不是外部的while循环,并且包含最后一个i ++,但是我没有'做T,在这里,因为我认为不好的风格。

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个转义的逗号)在你允许的时候d因为你可能最终会失去一些速度优势。

最终它可能不值得费心。

10

经过修改StringTokenizer班后,我无法找到一种方法来满足返回["dog", "", "cat"]的要求。

此外,StringTokenizer级仅出于兼容性的原因,并鼓励使用String.split。从用于StringTokenizer的API规格:

StringTokenizer是传统类 ,但留作兼容性 原因虽然不鼓励使用在新代码 。建议任何寻求此功能的人都使用split方法 或Stringjava.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级别处理字符串可能会有一些性能改进。但是我会把它作为练习留给读者。

类也为了充分利用这是在Java 5中StringTokenizer推出for-each循环结构的实现IterableIteratorEnumerator,并且不支持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),和结果以下内容:

 
        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 

所以,可以从有限的测试中可以看出只有5次运行期间,StringTokenizer其实ç做欧米出局最快,但MyTokenizer进入第二。然后,String.split是最慢的,并且预编译的正则表达式比split方法稍快。

与任何小基准一样,它可能不是真实生活条件的代表,所以结果应该与盐的谷物(或丘)一起拍摄。

0

我会推荐Google的Guava Splitter
coobird检验比较,并得到以下结果:

的StringTokenizer 104
谷歌番石榴分配器142
String.split 446
正则表达式299