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

时间:2010-07-29 12:55:25

标签: java regex string language-design

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

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

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

  1. 每次调用方法时,它们都会重新编译表达式。因此,它们会对性能产生影响。
  2. 我发现在大多数“真实”情境中,这些方法都是用“固定”文本调用的。分裂方法最常见的用法更糟糕:通常使用单个字符(通常是',a';'或'&')进行分割。
  3. 因此,不仅默认方法功能强大,而且它们实际上用于实际使用的内容也显得过于强大。在内部,我们开发了一种“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竞技场中使用 big 网站的数据。所以对我来说这些东西真的很重要:)

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

    到目前为止我的结论:

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

    P.S。我正在通过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;
    }
    

9 个答案:

答案 0 :(得分:12)

请注意,每次都不需要重新编译正则表达式。来自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以使用一种方法而不是几种方法。如果有人担心性能,仍然可以预编译正则表达式,如上所示。

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

将有关正则表达式匹配器与固定字符串/字符的性能比较与专门用于这些的匹配器进行性能比较会很有趣。差异可能不足以证明单独实施的合理性。

答案 1 :(得分:12)

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

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

令人感到有趣的是,对于打字非常苛刻的语言造成了将字符串和正则表达式视为同一事物的错误。没有什么内置方法可以进行普通的字符串替换或拆分,这是不太有趣的。您必须使用带有String.replaceAll(String, String) d字符串的正则表达式替换。而且你甚至只能从Java 5开始。无望。

@Tim Pietzcker:

  

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

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

RegExp.quote

另外,当然Perl使用regexen绝对做到了所有事情,因为它只是像那样反常。

(这是一个评论而不仅仅是一个答案,但对于一个人来说太大了。为什么让Java做到这一点?不知道,他们在早期犯了很多错误。其中一些人有错误自从被修复以来,我怀疑他们是否考虑将正则表达式功能放在1.0中标记为split()的框中,s.split(findstr).join(replacestr) 的设计可以更清晰匹配。)

答案 2 :(得分:2)

我想有一个很好的理由是他们可以简单地将降压传递给正则表达式方法,该方法可以完成所有字符串方法的所有重要工作。我猜他们认为如果他们已经有了一个可行的解决方案,从开发和维护的角度来看,为每种字符串操作方法重新发明轮子的效率会降低。

答案 3 :(得分:2)

有趣的讨论!

Java最初并不打算作为批处理编程语言。因此,开箱即用的API更倾向于做一个“替换”,一个“解析”等,除了应用程序初始化时,应用程序可能需要解析一堆配置文件。

因此,在简单IMO的祭坛上牺牲了这些API的优化。但这个问题提出了一个重要的观点。 Python希望将正则表达式与其API中的非正则表达式区分开来,这源于Python也可以用作优秀的脚本语言。在UNIX中,fgrep的原始版本也不支持正则表达式。

我参与了一个项目,我们必须在java中完成一些ETL工作。那时候,我记得在你的问题中提出了你所提到的那种优化。

答案 4 :(得分:1)

在查看Java String类时,正则表达式的使用似乎是合理的,如果不需要正则表达式,还有其他选择:

http://java.sun.com/javase/6/docs/api/java/lang/String.html

boolean matches(String regex) - 正则表达式似乎合适,否则您只能使用equals

String replaceAll/replaceFirst(String regex, String replacement) - 有些等价物会改为使用CharSequence,从而阻止了正则表达式。

String[] split(String regex, int limit) - 一个功能强大但价格昂贵的分割,你可以使用StringTokenizer来分割标记。

这是我看到的唯一使用正则表达式的函数。

编辑:在看到StringTokenizer是遗留的之后,我会按照PéterTörök的回答来预编译正则表达式而不是使用标记器。

答案 5 :(得分:1)

我怀疑 String#split(String)这样的东西之所以使用regexp是因为它涉及Java类库中较少的无关代码。由,或空格之类的分割产生的状态机非常简单,使用 StringCharacterIterator 来执行的等效项不会比使用静态实现的等效项慢得多。

除此之外,静态实现的解决方案会使JIT的运行时优化变得复杂,因为它将是一个不同的代码块,也需要热代码分析。在库中定期使用现有的Pattern算法意味着它们更有可能成为JIT编译的候选者。

答案 6 :(得分:1)

非常好的问题..

我想,当设计师坐下来看这个时(似乎并不是很长时间),他们从一个角度来看它应该设计成适合尽可能多的不同可能性。正则表达式提供了灵活性。

他们没有考虑效率。有Java Community Process可以提出这个问题。

您是否考虑过使用java.util.regex.Pattern类,在此类中编译表达式一次,然后在不同的字符串上使用。

Pattern exp = Pattern.compile(":");
String[] array = exp.split(sourceString1);
String[] array2 = exp.split(sourceString2);

答案 7 :(得分:0)

您的问题的答案是Java核心API做错了。对于日常工作,您可以考虑使用Guava库的CharMatcher,它可以很好地填补空白。

答案 8 :(得分:0)

  

...为什么Java API会以现在的方式选择?

简短回答:事实并非如此。没有人决定在String API中使用正则表达式方法而不是非正则表达式方法,它就是这样解决的。

我总是理解Java的设计者故意将字符串操作方法保持在最低限度,以避免API膨胀。但是当JDK 1.4中出现正则表达式支持时,他们当然必须为String的API添加一些便利方法。

现在,用户面临着极其强大和灵活的正则表达式方法以及Java始终提供的骨骼基本方法之间的选择。