为什么Java中的大多数字符串操作都基于正则表达式?

2022-09-01 02:13:09

在Java中,有一堆方法都与操作字符串有关。最简单的例子是 String.split(“something”) 方法。

现在,许多这些方法的实际定义是,它们都采用正则表达式作为其输入参数。这使得所有非常强大的构建块。

现在,您可以在许多这些方法中看到两种效果:

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

因此,不仅默认方法功能强大,而且它们的实际用途似乎也过强。在内部,我们开发了一种“fastSplit”方法,可以在固定字符串上进行拆分。我在家里写了一个测试,看看如果已知它是单个字符,我能做得快多快。两者都比“标准”拆分方法快得多。

所以我在想:为什么Java API被选择成现在的样子?这样做的好理由是什么,而不是像split(char)和split(String)和splitRegex(String)这样的东西?


更新:我组合了几个调用,看看拆分字符串的各种方法需要多少时间。

简短总结:它有很大的不同!

我为每个测试用例进行了10000000次迭代,始终使用输入

"aap,noot,mies,wim,zus,jet,teun" 

并且始终使用 ',' 或 “,”作为拆分参数。

这是我在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

如您所见,如果您有很多“固定字符”拆分要做,则会有很大的不同。

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

我在这里没有考虑到的是垃圾收集器。据我所知,将正则表达式编译成模式/匹配器/.。会分配很多对象,需要收集一些时间。因此,也许从长远来看,这些版本之间的差异甚至更大....或更小。

到目前为止,我的结论是:

  • 仅当要拆分大量字符串时,才可对此进行优化。
  • 如果使用正则表达式方法,则在重复使用相同的模式时始终进行预编译。
  • 忘记(过时的)StringTokenizer
  • 如果要在单个字符上拆分,请使用自定义方法,特别是如果您只需要将其拆分为特定数量的部分(例如...2).

附言:我给你所有我自己种植的按字符方法分割的玩法(在许可证下,这个网站上的所有内容都属于:))。我从来没有完全测试过它们。。还。玩得愉快。

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

答案 1

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

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

Pattern.compile(regex).split(str, n) 

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

Pattern p = Pattern.compile(regex);
...
String[] tokens1 = p.split(str1); 
String[] tokens2 = p.split(str2); 
...

而不是

String[] tokens1 = str1.split(regex);
String[] tokens2 = str2.split(regex);
...

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

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

看到具有固定字符串/字符的正则表达式匹配器实现与专用于这些字符串/字符的匹配器的性能比较会很有趣。这种差异可能不够大,不足以证明单独实现的合理性。


答案 2

我不会说大多数字符串操作都是基于Java的正则表达式。实际上,我们只是在谈论 和 /。但我同意,这是一个很大的错误。splitreplaceAllreplaceFirst

除了低级语言功能(字符串)依赖于更高级别功能(正则表达式)的丑陋之外,对于新用户来说,这也是一个令人讨厌的陷阱,他们可能会自然而然地认为带有签名的方法将是字符串替换函数。在这种假设下编写的代码看起来就像在工作,直到一个正则表达式特殊字符悄悄进入,此时你遇到了令人困惑的,难以调试的(甚至可能是安全很重要的)错误。String.replaceAll(String, String)

有趣的是,一种对打字如此严格语言犯了一个草率的错误,将字符串和正则表达式视为同一回事。不那么有趣的是,仍然没有内置的方法可以进行普通的字符串替换或拆分。您必须使用正则表达式替换为 d 字符串。你甚至只能从Java 5开始得到它。绝望的。Pattern.quote

@Tim 皮茨克:

还有其他语言可以做同样的事情吗?

JavaScript的字符串部分以Java为蓝本,并且在的情况下也很混乱。通过传入字符串,您可以获得纯字符串替换,但它仅替换第一个匹配项,这很少是想要的。要获得替换所有内容,您必须使用标志传入对象,如果要从字符串动态创建它(JS中没有内置方法),这同样存在问题。幸运的是,它是纯粹基于字符串的,所以你可以使用这个成语:replace()RegExp/gRegExp.quotesplit()

s.split(findstr).join(replacestr)

当然,Perl绝对使用正则表达式做任何事情,因为它就是这样不合常理的。

(这是一个评论而不是一个答案,但对于一个人来说太大了。Java为什么要这样做?邓诺,他们在早期犯了很多错误。其中一些问题已经得到修复。我怀疑,如果他们想过将正则表达式功能放在1.0中标记回来的框中,那么的设计会更干净。PatternString