代码之家  ›  专栏  ›  技术社区  ›  Niels Basjes

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

  •  42
  • Niels Basjes  · 技术社区  · 15 年前

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

    现在,这些方法的实际定义是,它们都将正则表达式作为输入参数。这就形成了非常强大的积木。

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

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

    因此,不仅默认的方法很强大,而且它们似乎也被实际使用的方法所压倒。在内部,我们开发了一个“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
    

    正如您所看到的,如果您有很多“固定字符”拆分要做,那么这会产生很大的不同。

    为了给你们一些了解,我现在在ApacheLogfiles和Hadoop竞技场,这里有一个 大的 网站。所以对我来说这件事很重要:)

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

    我的结论是:

    • 只有在有很多字符串要拆分的情况下,才能优化此设置。
    • 如果使用regex方法,则如果重复使用相同的模式,则始终预编译。
    • 忘记(过时的)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;
    }
    
    9 回复  |  直到 15 年前
        1
  •  12
  •   Péter Török    15 年前

    请注意,不需要每次重新编译regex。从 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,使其具有一个方法而不是多个方法。如果有人担心性能问题,那么仍然可以如上面所示预编译regex。

    我的感觉(我不能用任何统计证据支持)是大多数情况 String.split() 在性能不是问题的上下文中使用。例如,它是一次性动作,或者与其他因素相比,性能差异可以忽略不计。IMO很少有这样的情况:在一个紧密的循环中,使用同一个regex对字符串进行数千次拆分,在这种情况下,性能优化确实是有意义的。

    将带有固定字符串/字符的regex matcher实现的性能与专门针对这些字符串/字符的matcher实现的性能进行比较是很有趣的。差异可能不够大,无法证明独立的实现是正确的。

        2
  •  12
  •   bobince    15 年前

    我不认为大多数字符串操作是基于爪哇的正则表达式。真的,我们只是在谈论 split replaceAll / replaceFirst . 但我同意,这是个大错误。

    除了低级语言特性(字符串)依赖于高级特性(regex)的丑陋之外,对于新用户来说,这也是一个令人讨厌的陷阱,他们可能会自然而然地认为具有签名的方法 String.replaceAll(String, String) 将是字符串替换函数。在这个假设下编写的代码看起来是有效的,直到有一个regex特殊字符潜入,这时您会发现一些令人困惑的、难以调试(甚至是安全性很高的)错误。

    有趣的是,一种对打字要求如此严格的语言犯了一个粗心的错误,即将字符串和正则表达式视为相同的东西。不那么有趣的是 仍然 没有用于执行纯字符串替换或拆分的内置方法。必须使用regex替换为 Pattern.quote D字符串。你甚至只能从Java 5中得到它。绝望的。

    @ Tim Pietzcker:

    其他语言也有同样的功能吗?

    JavaScript的字符串部分是基于Java的,而且在情况下也很混乱。 replace() . 通过传入一个字符串,可以得到一个普通的字符串替换,但它只替换第一个匹配项,这是很少需要的。为了得到一个替代品,你必须在 RegExp 对象与 /g 标志,如果要从字符串动态创建它(没有内置的 RegExp.quote JS中的方法。幸运的是, split() 纯粹是基于字符串的,因此您可以使用成语:

    s.split(findstr).join(replacestr)
    

    当然,Perl对Regexen做的每件事都是绝对正确的,因为它就是这样的反常行为。

    (这不是一个回答,而是一个评论,但对一个来说太大了。 为什么? Java是这样做的吗?不知道,他们在早期犯了很多错误。其中一些已经修复。我怀疑他们是否想把regex功能放在标记框中 Pattern 回到1.0,设计 String 比较干净。)

        3
  •  2
  •   Scott M.    15 年前

    我认为一个很好的理由是,它们可以简单地将责任转嫁给regex方法,它为所有的字符串方法做了所有真正的繁重工作。我猜他们认为,如果他们已经有了一个有效的解决方案,那么从开发和维护的角度来看,为每一种弦操作方法重新发明轮子的效率会降低。

        4
  •  2
  •   raja kolluru    15 年前

    有趣的讨论!

    Java最初不是作为批处理编程语言而设计的。因此,开箱即用的API更倾向于执行一个“替换”、一个“解析”等操作,除非在应用程序初始化时,应用程序可能会解析一组配置文件。

    因此,这些API的优化被牺牲在了简单的圣坛上,但这个问题提出了一个重要的观点。python希望在其API中保持regex与非regex的区别,这是因为python也可以作为一种优秀的脚本语言使用。在UNIX中,fgrep的原始版本也不支持regex。

    我参与了一个项目,我们必须在Java中做一些ETL工作。那时,我记得在你的问题中,我提出了一些你提到过的优化方法。

        5
  •  1
  •   Alain O'Dea    15 年前

    我怀疑这就是为什么 字符串拆分(字符串) 在HUDE下使用ReXEP是因为它在Java类库中涉及较少的无关代码。由诸如 , 或者空间非常简单,执行起来不可能比使用 字符串字符迭代器 .

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

        6
  •  1
  •   Adrian Regan    15 年前

    很好的问题……

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

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

    您是否已经研究过使用java.util.regex.pattern类,在该类中编译表达式一次,然后在不同的字符串上使用。

    Pattern exp = Pattern.compile(":");
    String[] array = exp.split(sourceString1);
    String[] array2 = exp.split(sourceString2);
    
        7
  •  1
  •   Brandon Horsley    15 年前

    在查看Java字符串类时,正则表达式的使用似乎是合理的,如果不需要正则表达式,则存在替代方案:

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

    boolean matches(String regex) -一个regex似乎是合适的,否则你可以只使用 equals

    String replaceAll/replaceFirst(String regex, String replacement) -有一些等价物采用charsequence代替,防止regex。

    String[] split(String regex, int limit) -一个强大但昂贵的拆分,您可以使用StringTokenizer按标记进行拆分。

    这些是我看到的使用regex的唯一函数。

    编辑:在看到StringTokenizer是遗留的之后,我会遵从P_)ter t_¶r_¶k的回答,将regex预编译为split,而不是使用tokenizer。

        8
  •  0
  •   user18943    15 年前

    您的问题的答案是Java核心API出错了。在日常工作中,你可以考虑使用Guava图书馆的Charmatcher,它可以很好地填补空白。

        9
  •  0
  •   Alan Moore Chris Ballance    15 年前

    为什么Java API选择了它现在的方式?

    简短的回答:不是,从来没有人 果断的 为了使regex方法比字符串API中的非regex方法更受欢迎,它就是这样解决的。

    我总是理解Java的设计者故意将字符串操作方法保持在最低限度,以避免API膨胀。但是当JDK1.4中出现regex支持时,当然他们必须向字符串的API添加一些方便的方法。

    因此,现在用户在强大的灵活的正则表达式方法和Java总是提供的基本的基本方法之间面临着选择。