Java分割字符串的性能表现

53

这是我应用程序中的当前代码:

String[] ids = str.split("/");

在对应用进行性能分析时,有相当一部分时间会花费在字符串拆分上。此外,split方法接受一个正则表达式,在这里是多余的。

我可以使用什么替代方法来优化字符串拆分?StringUtils.split更快吗?

(我本可以自己尝试并测试,但分析我的应用程序需要很长时间。)

10个回答

60

String.split(String)如果您的模式只有一个字符长,将不会创建正则表达式。当按单个字符拆分时,它将使用专门的代码,这是非常高效的。StringTokenizer在这种特殊情况下并不快多少。

这是在OpenJDK7/OracleJDK7中引入的。这里是一个错误报告一个提交。我在这里做了一个简单的基准测试


$ java -version
java version "1.8.0_20"
Java(TM) SE Runtime Environment (build 1.8.0_20-b26)
Java HotSpot(TM) 64-Bit Server VM (build 25.20-b23, mixed mode)

$ java Split
split_banthar: 1231
split_tskuzzy: 1464
split_tskuzzy2: 1742
string.split: 1291
StringTokenizer: 1517

2
感谢这个基准测试。不过你的代码有些“不公平”,因为StringTokenizer部分避免了创建List并将其转换为数组...不过这是一个很好的起点! - Yossi Farjoun
13
为了避免在split方法内创建正则表达式,仅使用一个字符长度的模式是不够的。该字符还不能是正则表达式元字符 ".$|()[{^?*+\"之一,例如split(".")将创建/编译正则表达式模式。(在至少jdk8上进行了验证) - andrii
在我的Java 8版本中,它是这样的。从拆分实现注释中可以看到:如果正则表达式是(1)一个字符的字符串,并且该字符不是RegEx的元字符".$|()[{^?*+\ "之一,或者(2)两个字符的字符串并且第一个字符是反斜杠,第二个字符不是ASCII数字或ASCII字母,则使用快速路径。 - David Bradley
1
添加限定条件。如果您只是输入“|”,那么它将被视为正则表达式。但是“\|”不会被视为正则表达式。这一点起初让我感到困惑。 - David Bradley
至少 split_banthar(已使用复制/粘贴代码进行测试)的行为与 JAVA SPLIT 不同... - marcolopes

24

如果你可以使用第三方库,GuavaSplitter在不需要时不会产生正则表达式的额外开销,并且通常非常快速。(声明:我对Guava有所贡献。)

Iterable<String> split = Splitter.on('/').split(string);

此外,Splitter通常比String.split更加可预测,详见这里


2
这对我在处理大文件中的行时产生了非常显著的影响。 - John Humphreys
4
此帖建议不要使用 Iterable,即使 Guava 的团队领导也这样说... http://alexruiz.developerblogs.com/?p=2519 - sirvon
1
博客文章已经消失了,但是在互联网档案馆中有一个快照可用。 - peq

9

StringTokenizer 在这种简单的解析情况下要快得多(我之前进行了一些基准测试,并且可以获得巨大的加速效果)。

StringTokenizer st = new StringTokenizer("1/2/3","/");
String[] arr = new String[st.countTokens()];
arr[0] = st.nextToken();

如果你想提高一点性能,你也可以手动操作:

String s = "1/2/3"
char[] c = s.toCharArray();
LinkedList<String> ll = new LinkedList<String>();
int index = 0;

for(int i=0;i<c.length;i++) {
    if(c[i] == '/') {
        ll.add(s.substring(index,i));
        index = i+1;
    }
}

String[] arr = ll.size();
Iterator<String> iter = ll.iterator();
index = 0;

for(index = 0; iter.hasNext(); index++)
    arr[index++] = iter.next();

4
StringTokenizer是一个遗留类,为了兼容性而保留,尽管在新代码中不鼓励使用它。建议寻求此功能的任何人改用String的split方法或java.util.regex包。 - Nandkumar Tekale
4
仅因其传统并不意味着它没有用处。事实上,这个特定的类对于额外的性能提升非常有用,所以我实际上反对使用“传统”的标签。 - tskuzzy
5
String类的split方法和java.util.regex包使用正则表达式会产生相当大的开销。而StringTokenizer则不会。 - Louis Wasserman
2
@tskuzzy,无论你是否反对“遗留”标签,都不重要,因为Javadoc说:它的使用是不鼓励的。 - Nandkumar Tekale
2
@NandkumarTekale 你似乎没有理解我的观点。但如果你想避免使用“遗留”类而选择“慢”的类,那是你的选择。 - WestCoastProjects
显示剩余5条评论

8

考虑到我正在处理大规模的数据,我认为提供更多基准测试会有所帮助,包括一些我自己的实现(我是按空格分割的,但这应该说明了通常需要多长时间):

我正在处理一个大小为426 MB,共2622761行的文件。唯一的空白字符是普通的空格(" ")和换行符("\n")。

首先,我将所有换行符替换为空格,并对解析一个巨大的行进行基准测试:

.split(" ")
Cumulative time: 31.431366952 seconds

.split("\s")
Cumulative time: 52.948729489 seconds

splitStringChArray()
Cumulative time: 38.721338004 seconds

splitStringChList()
Cumulative time: 12.716065893 seconds

splitStringCodes()
Cumulative time: 1 minutes, 21.349029036000005 seconds

splitStringCharCodes()
Cumulative time: 23.459840685 seconds

StringTokenizer
Cumulative time: 1 minutes, 11.501686094999997 seconds

然后我逐行基准测试分割线(这意味着函数和循环被多次执行,而不是一次性执行所有操作):

.split(" ")
Cumulative time: 3.809014174 seconds

.split("\s")
Cumulative time: 7.906730124 seconds

splitStringChArray()
Cumulative time: 4.06576739 seconds

splitStringChList()
Cumulative time: 2.857809996 seconds

Bonus: splitStringChList(), but creating a new StringBuilder every time (the average difference is actually more like .42 seconds):
Cumulative time: 3.82026621 seconds

splitStringCodes()
Cumulative time: 11.730249921 seconds

splitStringCharCodes()
Cumulative time: 6.995555826 seconds

StringTokenizer
Cumulative time: 4.500008172 seconds

以下是代码:

// Use a char array, and count the number of instances first.
public static String[] splitStringChArray(String str, StringBuilder sb) {
    char[] strArray = str.toCharArray();
    int count = 0;
    for (char c : strArray) {
        if (c == ' ') {
            count++;
        }
    }
    String[] splitArray = new String[count+1];
    int i=0;
    for (char c : strArray) {
        if (c == ' ') {
            splitArray[i] = sb.toString();
            sb.delete(0, sb.length());
        } else {
            sb.append(c);
        }
    }
    return splitArray;
}

// Use a char array but create an ArrayList, and don't count beforehand.
public static ArrayList<String> splitStringChList(String str, StringBuilder sb) {
    ArrayList<String> words = new ArrayList<String>();
    words.ensureCapacity(str.length()/5);
    char[] strArray = str.toCharArray();
    int i=0;
    for (char c : strArray) {
        if (c == ' ') {
            words.add(sb.toString());
            sb.delete(0, sb.length());
        } else {
            sb.append(c);
        }
    }
    return words;
}

// Using an iterator through code points and returning an ArrayList.
public static ArrayList<String> splitStringCodes(String str) {
    ArrayList<String> words = new ArrayList<String>();
    words.ensureCapacity(str.length()/5);
    IntStream is = str.codePoints();
    OfInt it = is.iterator();
    int cp;
    StringBuilder sb = new StringBuilder();
    while (it.hasNext()) {
        cp = it.next();
        if (cp == 32) {
            words.add(sb.toString());
            sb.delete(0, sb.length());
        } else {
            sb.append(cp);
        }
    }

    return words;
}

// This one is for compatibility with supplementary or surrogate characters (by using Character.codePointAt())
public static ArrayList<String> splitStringCharCodes(String str, StringBuilder sb) {
    char[] strArray = str.toCharArray();
    ArrayList<String> words = new ArrayList<String>();
    words.ensureCapacity(str.length()/5);
    int cp;
    int len = strArray.length;
    for (int i=0; i<len; i++) {
        cp = Character.codePointAt(strArray, i);
        if (cp == ' ') {
            words.add(sb.toString());
            sb.delete(0, sb.length());
        } else {
            sb.append(cp);
        }
    }

    return words;
}

这是我使用StringTokenizer的方法:
    StringTokenizer tokenizer = new StringTokenizer(file.getCurrentString());
    words = new String[tokenizer.countTokens()];
    int i = 0;
    while (tokenizer.hasMoreTokens()) {
        words[i] = tokenizer.nextToken();
        i++;
    }

2
splitStringChList会丢弃最后一个字符串。 在返回之前添加以下代码:if (sb.length() > 0) words.add(sb.toString());另外:
  • 将sb.delete(0, sb.length());替换为sb.setLength(0);
  • 删除未使用的int i=0;
- Systemsplanet
此外,您应该只是从字符数组中的范围创建一个字符串,而不是使用 StringBuilder。我认为您的实现并不比 Java11 中的 split 更快。 - Luke

3

java.util.StringTokenizer(String str, String delim)这篇帖子称,速度大约快了一倍。

然而,除非您的应用程序规模巨大,否则使用split是可以的(参见同一帖子,引用数千个字符串只需几毫秒)。


不需要一个巨大规模的应用程序,只要有一个像文档解析器一样的紧密循环的分裂即可——而且是频繁的。想想解析twitter链接、电子邮件、hashtag等典型例程...它们被喂入几MB的文本进行解析。这个例程本身可能只有几十行代码,但每秒钟会被调用数百次。 - rupps

2

Guava提供了一个Splitter,比String.split()方法更灵活,并且不一定使用正则表达式。然而,在Java 7中,String.split()已经被优化,如果分隔符是单个字符,则避免使用正则表达式引擎。因此,在Java 7中性能应该相似。


哦,好的,我正在使用Java 5(不幸的是,无法更改)。 - Matthieu Napoli

1
使用Apache Commons Lang » 3.0的。
StringUtils.splitByWholeSeparator("ab-!-cd-!-ef", "-!-") = ["ab", "cd", "ef"]

如果您需要非正则表达式的分割,并且希望结果为字符串数组,则使用StringUtils,我比较了StringUtils.splitByWholeSeparator与Guava的Splitter和Java的String split,并发现StringUtils更快。
  1. StringUtils - 8毫秒
  2. String - 11毫秒
  3. Splitter - 1毫秒(但返回Iterable/Iterator,将它们转换为字符串数组总共需要54毫秒)

1

StringTokenizer比任何其他分割方法都要快,但是让分词器返回分隔符和标记化字符串可以提高大约50%的性能。这可以通过使用构造函数java.util.StringTokenizer.StringTokenizer(String str, String delim, boolean returnDelims)来实现。以下是有关此问题的其他见解:Java中StringTokenizer类与split方法的性能比较


0
字符串的split方法可能是更安全的选择。至少从java 6开始(尽管api参考是7),他们基本上说不鼓励使用StringTokenizer。他们的措辞如下所述。
StringTokenizer是一个遗留类,为了兼容性而保留,尽管在新代码中不鼓励使用它。建议任何寻求此功能的人改用String的split方法或java.util.regex包。

0

你可以自己编写split函数,这将是最快的解决方案。下面的链接证明了它的效果,我也亲测有效,可以优化我的代码6倍。

StringTokenizer - reading lines with integers

Split: 366毫秒 IndexOf: 50毫秒 StringTokenizer: 89毫秒 GuavaSplit: 109毫秒 IndexOf2(上述问题中提供的某些超级优化解决方案):14毫秒 CsvMapperSplit(逐行映射):326毫秒 CsvMapperSplit_DOC(构建一个文档并一次性映射所有行):177毫秒


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接