Java 模式匹配导致长输入上的堆栈溢出

Java 模式匹配导致长输入上的堆栈溢出,java,regex,stack-overflow,Java,Regex,Stack Overflow,我知道关于这个主题有几个线程,但是找到的解决方案是针对特定问题的,并且是基于改进正则表达式字符串的 无论如何,我需要处理一个文本输入文件,其中包含的数据模型将图形结构作为相邻列表。每行n包含一个与n相邻的顶点列表(每个顶点都记为整数),由一个或多个空格字符分隔。我决定在解析之前用一个正则表达式字符串检查每一行,而不是在出现错误数据时抛出并捕获numberformatceptions 这就是代码在这一点上的样子: line = line.trim(); //Remove whitespace in

我知道关于这个主题有几个线程,但是找到的解决方案是针对特定问题的,并且是基于改进正则表达式字符串的

无论如何,我需要处理一个文本输入文件,其中包含的数据模型将图形结构作为相邻列表。每行n包含一个与n相邻的顶点列表(每个顶点都记为整数),由一个或多个空格字符分隔。我决定在解析之前用一个正则表达式字符串检查每一行,而不是在出现错误数据时抛出并捕获
numberformatceptions

这就是代码在这一点上的样子:

line = line.trim(); //Remove whitespace in the beginning and in the end of line
if (Pattern.matches("(\\d+\\s*)*", line)){
    //split string and parse vertices
}
一行也可能为空,这意味着其度数为0


它在大多数情况下都非常有效,但在分析一条具有约1000个相邻顶点的线时失败(StackOverflow)。我想知道解决这个问题的所有方法。我不想增加JVM堆栈的大小,因为程序将保持可移植性。此外,我想继续使用正则表达式模式匹配,因为嘿,这就是它的用途!因此,希望有人有一个好主意。

您没有使用正则表达式来提取单个数字,因此您只需检查以确保行中只包含数字和空格即可。您的正则表达式可以简单到您想要使用的
“[\\d\\s]*”

^[\\d\\s]+$
基本上,这将匹配空格和数字序列,确保行首和行尾之间没有其他字符。您的原始测试需要3000多个步骤来解析我的测试字符串,这需要3个步骤

这产生了以下结果:

123123  123123  123123  123123 - OK
1 - OK
123g 12r3123 123 - NOT OK

已经提供了替代解决方案,因此我不打算讨论这些问题。我将解释为什么您的模式会导致
stackoverflowerrror
1,以及您的正则表达式如何也会遭受灾难性的回溯

如果JVM使用OpenJDK Java类库,则会发生1
StackOverflowerError
。对于使用其他内容作为类库的JVM,可能不会发生这种情况。但是,由于Oracle的JRE(最常见的JRE)使用OpenJDK Java类库(Oracle实际上维护OpenJDK),因此在编写正则表达式时必须考虑到这个错误。另一方面,这在过去曾多次被报告为一个bug,但至今仍未修复

不要接受这个答案,因为它不能解决问题

堆栈溢出错误 为了匹配由量词(除所有格量词外)重复的子模式,
模式
类可以递归调用内部函数进行匹配,因此每次迭代使用一些堆栈

它通常会对子模式进行一些分析,以避免对简单模式(如
\w+
(?:df)+
)进行递归调用,但它被迫对
(?:gd?f)*
(?:g | d | f)
进行递归调用。请注意,前一种情况没有选择点,但后一种情况有选择点。有选择点的常见模式是量词(不包括所有格)或可选的

如果输入字符串足够长,以致子模式重复数千次,那么您的模式将得到
stackoverflowerrror
,因为它包含选择点

所有格量词没有选择点,因为它不允许回溯。引擎不需要回溯,所以它不需要递归调用来在堆栈上存储信息

灾难性回溯 您的模式会遭受灾难性的回溯,因为您可以找到一个字符串,该字符串可以在不同的迭代次数中与您的正则表达式匹配。在匹配输入中,通常会探索一个或两个分支,但当输入字符串是一个失败的输入时,会探索所有这些分支

例如,让我们使用一个简单的输入,
1234
和regex
(\\d+\\s*)*
。上面的正则表达式可以用几种不同的方式匹配它

1/2/3/4    (4 iterations)
1/2/34     (3 iterations)
12/34      (2 iterations)
1234       (1 iteration)
1/23/4     (3 iterations)
在输入失败时,例如
1234 5678 x
,引擎将回溯您分割号码的所有方式。下面的跟踪显示了引擎尝试的前几次尝试:

1234 /5678 /x
1234 /567/8 /x
1234 /56/78 /x
1234 /56/7/8 /x
1234 /5/678 /x
1234 /5/67/8 /x
1234 /5/6/78 /x
1234 /5/6/7/8 /x

123/4 /5678 /x
123/4 /567/8 /x
123/4 /56/78 /x
123/4 /56/7/8 /x
123/4 /5/678 /x
...

如果输入由一个长的数字和许多这样的数字组成,您将进入回溯地狱。

如果您只是要检查,
“[\\d\\s]”
就足够了。是否也要使用正则表达式拆分字符串?仅仅使用JavaSplit函数就可以了。只是询问,因为您在正则表达式中使用了一个组。@acarlon稍后我将使用line.split(\\s+)来显示字符串。有没有合适的方法?这很好-您在正则表达式中使用了分组,这就是为什么我想知道您是否想要使用正则表达式进行拆分。+1,我得出了类似的结论-他不需要检查行的开头和结尾吗?编辑:它需要是[\\d\\s]*才能接受空进程空字符串,对吗?@user3184683:对不起,我错过了!谢谢,我们将提供与Gebe相同的解决方案。此处相同:它应该是[\\d\\s]*以避免忽略空行(因为空行是有效的,并且表示度为0的垂直)。有趣的。。。如果执行
Pattern.matches((\\d+\\s*){1,32})*“,line)
,会发生什么情况?所以它会保留所有这些,以防需要回溯。那么如果你使用所有格量词会发生什么呢?类似这样的内容:
“^(\\d++\\s*+)*++$”
。或者可能
“^(\\d++\\s*)*++$”
就足够了。@DavidKnipe:使用所有格量词重复的子模式在与子模式匹配时仍然会回溯。子模式匹配(迭代完成)后,它将丢弃子模式中的信息,因为它不需要。我认为引擎可能对所有格量词使用了循环,因为它只需要循环,直到它无法匹配子模式。我假设子模式匹配足够小,我们不需要担心它的堆栈大小