Java中substring、split、StringTokenizer三种截取字符串方法的性能比较

时间:2021-11-28 22:15:25

最近在阅读java.lang下的源码,读到String时,突然想起面试的时候曾经被人问过:都知道在大数据量情况下,使用String的split截取字符串效率很低,有想过用其他的方法替代吗?用什么替代?我当时的回答很斩钉截铁:没有。 
Google了一下,发现有2种替代方法,于是在这里我将对这三种方式进行测试。 
测试的软件环境为:Windows 10、Intellij IDEA、JDK1.8。 
测试用例使用类ip形式的字符串,即3位一组,使用”.”间隔。数据分别使用:5组、10组、100组、1000组、10000组、100000组。

实现

闲话不说,先上代码:

import java.util.Random;
import java.util.StringTokenizer;

/** * String测试类 * @author yuitang * */
public class StringTest {

    public static void main(String args[]){
        String orginStr = getOriginStr(5);

        //////////////String.split()表现//////////////////////////////////////////////
        System.out.println("使用String.split()切分字符串");
        long st1 = System.nanoTime();
        String [] result = orginStr.split("\\.");
        System.out.println("String.split()截取字符串用时:" + (System.nanoTime()-st1));
        System.out.println("String.split()截取字符串结果个数:" + result.length);
        System.out.println();

        //////////////StringTokenizer表现//////////////////////////////////////////////
        System.out.println("使用StringTokenizer切分字符串");
        long st3 = System.nanoTime();
        StringTokenizer token=new StringTokenizer(orginStr,".");
        System.out.println("StringTokenizer截取字符串用时:"+(System.nanoTime()-st3));
        System.out.println("StringTokenizer截取字符串结果个数:" + token.countTokens());
        System.out.println();

        ////////////////////String.substring()表现//////////////////////////////////////////
        long st5 = System.nanoTime();
        int len = orginStr.lastIndexOf(".");
        System.out.println("使用String.substring()切分字符串");
        int k=0,count=0;

        for (int i = 0; i <= len; i++) {
            if(orginStr.substring(i, i+1).equals(".")){
                if(count==0){
                    orginStr.substring(0, i);
                }else{
                    orginStr.substring(k+1, i);
                    if(i == len){
                        orginStr.substring(len+1, orginStr.length());
                    }
                }
                k=i;count++;
            }
        }
        System.out.println("String.substring()截取字符串用时:"+(System.nanoTime()-st5));
        System.out.println("String.substring()截取字符串结果个数:" + (count + 1));
    }

    /** * 构造目标字符串 * eg:10.123.12.154.154 * @param len 目标字符串组数(每组由3个随机数组成) * @return */
    private static String getOriginStr(int len){

        StringBuffer sb = new StringBuffer();
        StringBuffer result = new StringBuffer();
        Random random = new Random();
        for(int i = 0; i < len; i++){
            sb.append(random.nextInt(9)).append(random.nextInt(9)).append(random.nextInt(9));
            result.append(sb.toString());
            sb.delete(0, sb.length());
            if(i != len-1)
                result.append(".");
        }

        return result.toString();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74

改变目标数据长度修改getOriginStr的len参数即可。 
5组测试数据结果如下图:

Function 5 10 100 1000 10000 100000
split 45792 50134 155534 1035446 5395136 23128381
StringTokenizer 24080 24870 41845 62766 44608 30002
substring 35528 37896 156324 838463 8575293 25692324

下面这张图对比了下,split耗时为substring和StringTokenizer耗时的倍数:

Tables split StringTokenizer substring
5   1.90 1.29
10   2.02 1.32
100   3.17 0.99
1000   16.50 1.23
10000   120.95 0.63
100000   770.89 0.90

结论

最终,StringTokenizer在截取字符串中效率最高,不论数据量大小,几乎持平。substring和split的效率几乎差别不大,甚至当数据量足够庞大的时候,substring的效率还比不上split。 
究其原因,split的实现方式是采用正则表达式实现,所以其性能会比较低。至于正则表达式为何低,还未去验证。split源码如下:

public String[] split(String regex) {
    return split(regex, 0);
}

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