最近在研究提升数据ETL入Hbase的效率,涉及文本处理都离不开split(或者说离不开indexof、substring)。当然我程序的瓶颈不在这,只是有空就看看,可发现网上有很多针对StringTokenizer和Split效率比较,有很多看着心塞,就来blog一下。转开发岗位前,一直用python和perl,现在重拾java,觉得烦死。
网上很多比较,初始条件不对等,忽略StringTokenizer少了Split为遍历分组建立内存存储,如下:
ArrayList list = new ArrayList<>
return list.subList(0, resultSize).toArray(result);
具体请看,String源码
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0;
int next = 0;
boolean limited = limit > 0;
ArrayList list = new ArrayList<>();
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
//assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// Add remaining segment
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0)
while (resultSize > 0 && list.get(resultSize - 1).length() == 0)
resultSize--;
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
为了条件对等,我写了以下函数
public static String[] splitString(String str,String delimiter){
StringTokenizer stoken = new StringTokenizer(str,delimiter);
String[] result = new String[stoken.countTokens()];
int index = 0;
while(stoken.hasMoreTokens()){
result[index] = stoken.nextToken();
index++;
}
return result;
}
然后简单设计一个payload,如下:
*…………………….6.7.8.9.0……………………………………………………………….
*
注意:使用split(“.”,-1) 即limit=-1
结果:
split比StringTokenizer快3倍,这里splitString慢在index++,如果改为split源码中使用ArrayList的方式,还是会慢很多。
网上有说,StringTokenizer在处理长字符串会比split效率高很多。这里,又神伤一会,请看源码。老实说,处理长字符串,是split硬伤,还是那两行代码,如下:
ArrayList list = new ArrayList
return list.subList(0, resultSize).toArray(result);
大家应该知道,使用ArrayList会有什么后果吧,当size>capacity会引发扩容,然后就是System.copyArray,这个消耗很长的时间。所以split很怕这个,所以设了个limit,尽量忽略字符串后的null。请允许我二次元一下,如果某个网站的搜索框允许很长很长的查询会有什么后果呢,或者你绕开检查,设计一个无限长payload,会不会搞个jvm outofmemory。
其实,split已经做得很好了,记得N年前,spit还只是一行代码,就是
return Pattern.compile(regex).split(this, limit);
直接就是模式匹配去了,也不知道java和perl在正则方面,谁会更有优势。总之,用模式匹配,就是慢了去。现在,对于单个特殊字符分隔符,请尽情用split吧。于是乎,针对这种,我把源码的判断去掉,写成:
public static String[] splitString3(String str,String delimiter){
if(delimiter.length() == 1){
char ch = delimiter.charAt(0);
int off = 0;
int next = 0;
ArrayList list = new ArrayList<>();
while((next = str.indexOf(ch, off)) != -1){
list.add(str.substring(off,next));
off = next + 1;
}
if(off == 0){
return new String[]{str};
}
if(off <= str.length()){
list.add(str.substring(off, str.length()));
}
String[] result = new String[list.size()];
return list.subList(0, list.size()).toArray(result);
}
return str.split(delimiter,-1);
}
会不会很傻?速度是,1亿条数据,提升了1秒。当然现在是大数据时代,起码有100亿条数据吧。哇,100秒了。
这里,应该加个“其实”。速度,还是要跟实际需求走,很多时候我们并不需要全部分组,只需要其中几个字段而已,当然最好是开头几个。如果这样,我们就可以减少split的时间大头(一开始说的ArrayList)。请看如下代码:
public static Map getSplitString(String str,String delimiter,Map needMap){
Set needCols = needMap.keySet();
int len = needCols.size();
Map resultMap = new HashMap(len);
if(delimiter.length() == 1){
char ch = delimiter.charAt(0);
int off = 0;
int next = 0;
int index = 0;
while((next = str.indexOf(ch, off)) != -1){
if(needCols.contains(index))
resultMap.put(needMap.get(index), str.substring(off,next));
index++;
if(resultMap.keySet().size() == len)
break;
off = next + 1;
}
if(off == 0){
return resultMap;
}
if(off <= str.length() && resultMap.keySet().size() != len){
if(needCols.contains(index))
resultMap.put(needMap.get(index), str.substring(off,str.length()));
}
return resultMap;
}
int index = 0;
for(String element:str.split(delimiter,-1)){
if(needCols.contains(index) resultMap.put(needMap.get(index), e
lement);
index++;
}
return resultMap;
}
注:如果你更喜欢EnumMap,请改用
如果你只取开头四个字段,getSplitString速度会比split提升3倍。如果取最后四个字段,getSplitString速度也会有一些提升。
所以,建议你在处理海量数据的时候,将你想要的放在行首。
所以,建议你尽量用indexof 和substring。
注:为了避免制造网络垃圾,如果你看后觉得这是篇废话,请知会,我删除。或者,有更多意见,我及时修改。