为什么基于regexp的Java中的大多数字符串操作?

Nie*_*jes 42 java regex string language-design

在Java中,有许多方法都与操作字符串有关.最简单的例子是String.split("something")方法.

现在许多方法的实际定义是它们都将正则表达式作为它们的输入参数.这使得所有非常强大的构建块成为可能.

现在,您将在许多方法中看到两种效果:

  1. 每次调用方法时,它们都会重新编译表达式.因此,它们会对性能产生影响.
  2. 我发现在大多数"现实生活"的情况下,这些方法都被称为"固定"文本.拆分方法最常见的用法更糟糕:通常使用单个字符(通常是',a';'或'&')来调用它.

因此,不仅默认方法功能强大,而且它们实际上用于实际使用的内容也显得过于强大.在内部,我们开发了一种"fastSplit"方法,可以拆分固定字符串.我在家里写了一个测试,看看如果知道它是一个单一的字符,我能做多快.两者都明显快于"标准"分割方法.

所以我想知道:为什么Java API会以现在的方式选择?有什么理由去做这个而不是像split(char)和split(String)以及splitRegex(String)这样的东西?


更新:我打了几个电话,看看分割字符串的各种方法需要多长时间.

简短摘要:它有很大的不同!

我为每个测试用例做了10000000次迭代,总是使用输入

"aap,noot,mies,wim,zus,jet,teun" 
Run Code Online (Sandbox Code Playgroud)

并始终使用','或","作为拆分参数.

这是我在我的Linux系统上得到的(它是一个Atom D510盒子,所以它有点慢):

fastSplit STRING
Test  1 : 11405 milliseconds: Split in several pieces
Test  2 :  3018 milliseconds: Split in 2 pieces
Test  3 :  4396 milliseconds: Split in 3 pieces

homegrown fast splitter based on char
Test  4 :  9076 milliseconds: Split in several pieces
Test  5 :  2024 milliseconds: Split in 2 pieces
Test  6 :  2924 milliseconds: Split in 3 pieces

homegrown splitter based on char that always splits in 2 pieces
Test  7 :  1230 milliseconds: Split in 2 pieces

String.split(regex)
Test  8 : 32913 milliseconds: Split in several pieces
Test  9 : 30072 milliseconds: Split in 2 pieces
Test 10 : 31278 milliseconds: Split in 3 pieces

String.split(regex) using precompiled Pattern
Test 11 : 26138 milliseconds: Split in several pieces 
Test 12 : 23612 milliseconds: Split in 2 pieces
Test 13 : 24654 milliseconds: Split in 3 pieces

StringTokenizer
Test 14 : 27616 milliseconds: Split in several pieces
Test 15 : 28121 milliseconds: Split in 2 pieces
Test 16 : 27739 milliseconds: Split in 3 pieces
Run Code Online (Sandbox Code Playgroud)

正如你所看到的那样,如果你有很多"固定字符"分割,它会产生很大的不同.

给你们一些见解; 我目前在Apache日志文件和Hadoop竞技场中拥有大型网站的数据.所以对我来说这个东西真的很重要:)

我没有考虑过的东西是垃圾收集器.据我所知,将正则表达式编译成Pattern/Matcher/..会分配很多对象,需要在一段时间内收集.因此,从长远来看,这些版本之间的差异可能更大......或更小.

我的结论到目前为止:

  • 如果您要分割很多字符串,则只对此进行优化.
  • 如果您重复使用相同的模式,如果使用正则表达式方法,请始终预编译.
  • 忘记(过时的)StringTokenizer
  • 如果要分割单个字符,则使用自定义方法,尤其是如果您只需将其拆分为特定数量的片段(如... 2).

PS我给你所有我自己开发的char方法分开玩(根据许可证,本网站上的所有内容都属于:)).我还没有完全测试过它们.玩得开心.

private static String[]
        stringSplitChar(final String input,
                        final char separator) {
    int pieces = 0;

    // First we count how many pieces we will need to store ( = separators + 1 )
    int position = 0;
    do {
        pieces++;
        position = input.indexOf(separator, position + 1);
    } while (position != -1);

    // Then we allocate memory
    final String[] result = new String[pieces];

    // And start cutting and copying the pieces.
    int previousposition = 0;
    int currentposition = input.indexOf(separator);
    int piece = 0;
    final int lastpiece = pieces - 1;
    while (piece < lastpiece) {
        result[piece++] = input.substring(previousposition, currentposition);
        previousposition = currentposition + 1;
        currentposition = input.indexOf(separator, previousposition);
    }
    result[piece] = input.substring(previousposition);

    return result;
}

private static String[]
        stringSplitChar(final String input,
                        final char separator,
                        final int maxpieces) {
    if (maxpieces <= 0) {
        return stringSplitChar(input, separator);
    }
    int pieces = maxpieces;

    // Then we allocate memory
    final String[] result = new String[pieces];

    // And start cutting and copying the pieces.
    int previousposition = 0;
    int currentposition = input.indexOf(separator);
    int piece = 0;
    final int lastpiece = pieces - 1;
    while (currentposition != -1 && piece < lastpiece) {
        result[piece++] = input.substring(previousposition, currentposition);
        previousposition = currentposition + 1;
        currentposition = input.indexOf(separator, previousposition);
    }
    result[piece] = input.substring(previousposition);

    // All remaining array elements are uninitialized and assumed to be null
    return result;
}

private static String[]
        stringChop(final String input,
                   final char separator) {
    String[] result;
    // Find the separator.
    final int separatorIndex = input.indexOf(separator);
    if (separatorIndex == -1) {
        result = new String[1];
        result[0] = input;
    }
    else {
        result = new String[2];
        result[0] = input.substring(0, separatorIndex);
        result[1] = input.substring(separatorIndex + 1);
    }
    return result;
}
Run Code Online (Sandbox Code Playgroud)

Pét*_*rök 12

请注意,每次都不需要重新编译正则表达式.来自Javadoc:

调用此表单的方法会str.split(regex, n)产生与表达式相同的结果

Pattern.compile(regex).split(str, n) 
Run Code Online (Sandbox Code Playgroud)

也就是说,如果您担心性能,可以预先编译该模式,然后重复使用它:

Pattern p = Pattern.compile(regex);
...
String[] tokens1 = p.split(str1); 
String[] tokens2 = p.split(str2); 
...
Run Code Online (Sandbox Code Playgroud)

代替

String[] tokens1 = str1.split(regex);
String[] tokens2 = str2.split(regex);
...
Run Code Online (Sandbox Code Playgroud)

我相信这个API设计的主要原因是方便.由于正则表达式也包括所有"固定"字符串/字符,因此它简化了API以使用一种方法而不是几种方法.如果有人担心性能,正如上面所示,仍然可以预编译正则表达式.

我的感觉(我无法用任何统计证据支持)是大多数情况String.split()都用在性能不是问题的环境中.例如,它是一次性动作,或者与其他因素相比,性能差异可以忽略不计.IMO很少见的情况是你在紧密的循环中使用相同的正则表达式分割字符串数千次,其中性能优化确实有意义.

看到正则表达式匹配器实现与固定字符串/字符的性能比较与专门用于这些的匹配器的性能比较将是有趣的.差异可能不足以证明单独实施的合理性.


bob*_*nce 12

我不会说大多数字符串操作都是基于正则表达式的Java.真的,我们只是谈论splitreplaceAll/ replaceFirst.但我同意,这是一个很大的错误.

除了让低级语言特征(字符串)变得依赖于更高级别的特征(正则表达式)的丑陋之外,对于可能自然地认为具有签名的方法String.replaceAll(String, String)将是字符串的新用户而言,这也是一个令人讨厌的陷阱.替换功能.在这个假设下编写的代码看起来似乎有效,直到一个正则表达式特殊字符进入,此时你会遇到令人困惑,难以调试(甚至可能是安全性很大)的错误.

令人感到有趣的是,对于打字这种迂回严格的语言造成了将字符串和正则表达式视为同一事物的错误.这是不太有趣的是有还是做一个普通的字符串替换或分裂没有内置的方法.您必须使用Pattern.quoted字符串替换正则表达式.而且你甚至只能从Java 5开始.绝望.

@Tim Pietzcker:

是否有其他语言也这样做?

JavaScript的字符串部分以Java为模型,在这种情况下也很混乱replace().通过传入一个字符串,你得到一个普通的字符串替换,但它只替换了第一个匹配,这很少是想要的.要获得替换 - 所有你必须传入一个RegExp带有/g标志的对象,如果你想从一个字符串动态地创建它,它也会有问题(RegExp.quoteJS中没有内置方法).幸运的是,split()纯粹基于字符串,所以你可以使用成语:

s.split(findstr).join(replacestr)
Run Code Online (Sandbox Code Playgroud)

当然,Perl绝对可以用regexen完成所有工作,因为它只是那样的反常.

(这是一个评论而不仅仅是一个答案,但对于一个人来说太大了.为什么 Java会这样做?不知道,他们在早期犯了很多错误.其中一些已被修复.我怀疑他们是不是我认为将正则表达式功能放在Pattern1.0 中标记的方框中,设计String将更加清晰.)