为什么Java正则表达式引擎会在+重复上抛出StringIndexOutOfBoundsException?

时间:2010-09-13 07:10:05

标签: java regex fibonacci nested-reference

我写了一个正则表达式模式来找到Fibonacci数字(这没关系,为什么,我刚刚做了)。它按预期工作得非常好(see on ideone.com):

    String FIBONACCI = 
        "(?x) .{0,2} | (?: (?=(\\2?)) (?=(\\2\\3|^.)) (?=(\\1)) \\2)++ . ";

    for (int n = 0; n < 1000; n++) {
        String s = new String(new char[n]);
        if (s.matches(FIBONACCI)) {
            System.out.print(n + " ");
        }
    } // 0 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 

possessive重复(即主“循环”上的++)至关重要,因为您不希望使用此匹配算法进行回溯。但是,使重复回溯(即在主“循环”上只有+)不会产生不匹配,而是导致运行时异常! (as seen on ideone.com):

Exception in thread "main" java.lang.StringIndexOutOfBoundsException:
    String index out of range: -1

    at java.lang.String.charAt(String.java:686)
    at java.lang.Character.codePointAt(Character.java:2335)
    at java.util.regex.Pattern$CharProperty.match(Pattern.java:3344)
    at java.util.regex.Pattern$GroupCurly.match0(Pattern.java:3994)
    at java.util.regex.Pattern$GroupCurly.match0(Pattern.java:3966)
    at java.util.regex.Pattern$GroupCurly.match(Pattern.java:3916)
    at java.util.regex.Pattern$Branch.match(Pattern.java:4114)
    at java.util.regex.Matcher.match(Matcher.java:1127)
    at java.util.regex.Matcher.matches(Matcher.java:502)
    at java.util.regex.Pattern.matches(Pattern.java:930)
    at java.lang.String.matches(String.java:2090)

有人能解释一下这里发生了什么吗?这是Java正则表达式引擎中的错误吗?

1 个答案:

答案 0 :(得分:11)

错误ID 6984178

发动机投掷StringIndexOutOfBoundsExceptionsee: search results有很多与此相关的错误。特别报告此内容并在内部接受为Bug ID 6984178(可能需要一段时间才能显示在外部数据库中)。

这是一个更简单的模式,可以重现错误(see also on ideone.com):

System.out.println(
   "abaab".matches("(?x) (?: (?=(a+)) \\1 b )* x")
); // StringIndexOutOfBounds: -1

请注意,使用*?*+只会按预期返回false

看起来这个问题是由于在前瞻中有一个对捕获组的引用而试图回溯贪婪重复时触发的:越界索引是第一个和第二个{{1}之间的长度差异(例如a+得到"aabaaaaab")。

可能需要调试java.util.regex.Pattern source code以确定错误的确切性质。


探索斐波纳契模式

在Java引擎上,使用贪婪的回溯-3

这里有一个更详细的片段,展示引擎如何在这种模式下获得疯狂:

+

(稍加编辑)输出为(as seen on ideone.com):

String FIBONACCI = 
    "(?x) .{0,2} | (?: (?=(\\2|^)) (?=(\\2\\3|^.)) (?=(\\1)) \\2)+ . ";

for (int n = 0; n < 1000; n++) {
    String s = new String(new char[n]);
    try {
        if (s.matches(FIBONACCI)) {
            System.out.printf("%n%s", n);
        }
    } catch (StringIndexOutOfBoundsException e) {
        String index = e.getMessage().replace("String index out of range: ", "");
        System.out.printf(" <%s:%s>", n, index);
    }
}

所以引擎试图以-1,-3,-8,-21,-55,-144等方式访问字符串索引。请注意,这些是其他所有斐波纳契数,但是为负数。另请注意,除了前几个数字之外,其余的匹配(6,14,35,...)都是 NOT Fibonacci数。


在.NET引擎上,使用贪婪的回溯0 1 2 3 <5:-1> 6 <7:-1> ... <12:-1> <13:-3> 14 <15:-3> ... <33:-3> <34:-8> 35 <36:-8> ... <88:-8> <89:-21> 90 <91:-21> ... <232:-21> <233:-55> 234 <235:-55> ... <609:-55> <610:-144> 611 <612:-144> ...

虽然最初编写模式时考虑到占有量词的必要性,但实际上回溯重复仍然会产生正确的答案(假设引擎不像Java那样错误)。这是.NET引擎上的C#实现(see also on ideone.com):

+

正如您所看到的,即使使用回溯Regex r = new Regex( @"(?x) ^.{0,1}$ | ^(?: (?=(\2?)) (?=(\2\3|^.)) (?=(\1)) \2)+ . $ " ); for (int n = 0; n < 1000; n++) { if (r.IsMatch("".PadLeft(n))) { Console.Write("{0} ", n); } } // 0 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 “循环”,输出也是正确的。事实上,正是因为它是一个回溯循环,特殊情况可以仅限于+而不是.{0,1}


在Java引擎上,不情愿地回溯.{0,2}

这在Java中按预期工作。此外,由于它不情愿,我们还可以将特殊情况限制为+?see also on ideone.com):

.{0,1}

关于算法

公式

该模式利用了Second Identity of Fibonacci Numbers

alt text

这可以通过归纳来证明。

模式

让我们使用这个版本的模式(在Java中工作,锚定时也适用于C#):

String FIBONACCI = 
        "(?x) .{0,1} | (?: (?=(\\2|^)) (?=(\\2\\3|^.)) (?=(\\1)) \\2)+? . ";

基于 summation free-space! "loop" ↓ ↓ (?x) .{0,1} | (?: (?=(\2|^)) (?=(\2\3|^.)) (?=(\1)) \2)+? . \____/ \___________________________________/ ↑ ↑ base case inductive case +Fi +1 for n = 0,1 (assertions don't "count" toward sum)! $1 := $2 (or initialized with 0) $2 := $2+$3 (or initialized with 1) $3 := $1 (a temporary variable for the "swap") 转换,Fibonacci序列生成非常简单。由于断言是按顺序执行的,因此引入了一个临时变量(就像单一赋值“伪代码”版本一样)。