StringTokenizer or Split

最近在研究提升数据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。

注:为了避免制造网络垃圾,如果你看后觉得这是篇废话,请知会,我删除。或者,有更多意见,我及时修改。

你可能感兴趣的:(hbase,java,substring,split,java)