受到this的鼓励,以及我有数十亿字符串要解析的事实,我尝试修改我的代码以接受 StringTokenizer 而不是 String []
我和你之间唯一能够获得美味的x2性能提升的事实是,当你做的时候
"dog,,cat".split(",")
//output: ["dog","","cat"]
StringTokenizer("dog,,cat")
// nextToken() = "dog"
// nextToken() = "cat"
如何使用StringTokenizer获得类似的结果?有更快的方法吗?
答案 0 :(得分:12)
你真的只是用逗号来标记吗?如果是这样,我会编写自己的标记化器 - 它可能最终比更通用的StringTokenizer更有效,它可以查找多个标记,并且您可以使它按照您的喜好行事。对于这样一个简单的用例,它可以是一个简单的实现。
如果它有用,您甚至可以实施Iterable<String>
并通过强类型而不是Enumeration
提供的StringTokenizer
支持获得针对循环增强的支持。如果你想要任何帮助编码这样的野兽,请告诉我 - 这真的不应该太难。
此外,我尝试在实际数据上运行性能测试,然后再跳过现有解决方案。您是否知道在String.split
中实际花费了多少执行时间?我知道你有很多字符串需要解析,但是如果你之后对它们做了很多重要的事情,我希望它比分裂要重要得多。
答案 1 :(得分:10)
在修改StringTokenizer
课程后,我无法找到满足返回["dog", "", "cat"]
的要求的方法。
此外,仅出于兼容性原因而留下StringTokenizer
类,并且鼓励使用String.split
。来自StringTokenizer
的API规范:
StringTokenizer
是遗留类 为保持兼容性而保留 原因虽然它的用途是 在新的代码中气馁。它是 建议任何人都这样做 功能使用split
方法String
或java.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
将String
标记为String
并将String.indexOf
作为分隔符,并使用String.substring
方法执行分隔符搜索。标记由char[]
方法生成。
我怀疑通过在String
级而不是for-each
级别处理字符串可以提高性能。但我会把它作为练习留给读者。
该类还实现了Iterable
和Iterator
,以便利用Java 5中引入的StringTokenizer
循环结构。Enumerator
是for-each
},并且不支持StringTokenizer
构造。
是否更快?
为了找出这是否更快,我写了一个程序来比较以下四种方法的速度:
MyTokenizer
。String.split
。"dog,,cat"
。Pattern.compile
使用预编译的正则表达式。在四种方法中,字符串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"));
}