31

TL;博士

Matcher的 API背后的设计决策是什么?

背景

Matcher有一种我没有预料到的行为,而且我找不到很好的理由。API 文档说:

创建后,匹配器可用于执行三种不同类型的匹配操作: [...] 这些方法中的每一个都返回一个布尔值,指示成功或失败。更多关于匹配成功的信息可以通过查询匹配器的状态来获得。

API 文档进一步说的是:

匹配器的显式状态最初是未定义的;在成功匹配之前尝试查询它的任何部分将导致抛出 IllegalStateException。

例子

String s = "foo=23,bar=42";
Pattern p = Pattern.compile("foo=(?<foo>[0-9]*),bar=(?<bar>[0-9]*)");
Matcher matcher = p.matcher(s);
System.out.println(matcher.group("foo")); // (1)
System.out.println(matcher.group("bar"));

此代码抛出一个

java.lang.IllegalStateException: No match found

(1). 为了解决这个问题,有必要调用matches()或其他方法Matcher使group(). 以下作品:

String s = "foo=23,bar=42";
Pattern p = Pattern.compile("foo=(?<foo>[0-9]*),bar=(?<bar>[0-9]*)");
Matcher matcher = p.matcher(s);
matcher.matches(); // (2)
System.out.println(matcher.group("foo"));
System.out.println(matcher.group("bar"));

将调用添加到matches()at(2)会将调用设置Matcher为正确的状态group()

问题,可能没有建设性

为什么这个 API 是这样设计的?为什么在构建时不自动匹配?MatcherPatter.matcher(String)

4

6 回答 6

36

实际上,您误解了文档。第二次看你引用的声明: -

在成功匹配之前尝试查询它的任何部分将导致抛出 IllegalStateException。

如果未找到匹配项,则匹配器可能会IllegalStateException在访问时抛出。matcher.group()

因此,您需要使用以下测试来实际启动匹配过程:-

 - matcher.matches() //Or
 - matcher.find()

下面的代码: -

Matcher matcher = pattern.matcher();  

只需创建一个matcher实例。这实际上不会匹配字符串。哪怕是一场成功的比赛。因此,您需要检查以下条件,以检查成功匹配:-

if (matcher.matches()) {
    // Then use `matcher.group()`
}

如果if返回条件中的条件,则false表示没有匹配。所以,如果你在matcher.group()没有检查这个条件IllegalStateException的情况下使用,你会得到如果没有找到匹配项。


假设,如果Matcher按照您所说的方式设计,那么您必须null检查是否找到匹配项,然后调用matcher.group(),如下所示:-

你认为应该做的方式:-

// Suppose this returned the matched string
Matcher matcher = pattern.matcher(s);  

// Need to check whether there was actually a match
if (matcher != null) {  // Prints only the first match

    System.out.println(matcher.group());
}

但是,如果你想打印任何进一步的匹配,因为一个模式可以在一个字符串中多次匹配,为此,应该有一种方法告诉匹配器找到下一个匹配。但null支票将无法做到这一点。为此,您必须将匹配器向前移动以匹配下一个字符串。因此,在Matcher类中定义了各种方法来达到目的。该matcher.find()方法匹配字符串,直到找到所有匹配项。

还有其他方法,即match字符串以不同的方式,这取决于您要如何匹配。因此,它最终会在Matcher课堂上matching对字符串进行处理。Pattern类只是创建一个pattern匹配。如果Pattern.matcher()match模式,那么必须有某种方式来定义各种方式match,就像matching可以以不同的方式一样。于是,就需要Matcher上课了。

所以,它实际上是这样的: -

Matcher matcher = pattern.matcher(s);

   // Finds all the matches until found by moving the `matcher` forward
while(matcher.find()) {
    System.out.println(matcher.group());
}

因此,如果在字符串中找到 4 个匹配项,您的第一种方式将仅打印第一个,而第二种方式将通过matcher向前移动以匹配下一个模式来打印所有匹配项。

我希望这能说清楚。

类的文档Matcher描述了它提供的三种方法的使用,其中说: -

通过调用模式的 matcher 方法从模式创建匹配器。创建后,匹配器可用于执行三种不同类型的匹配操作:

  • matches 方法尝试将整个输入序列与模式进行匹配。

  • lookingAt 方法尝试将输入序列从开头开始与模式进行匹配。

  • find 方法扫描输入序列,寻找与模式匹配的下一个子序列。

不幸的是,我无法找到任何其他官方消息来源,明确说明此问题的原因方式

于 2012-10-16T09:33:28.380 回答
6

我的回答与 Rohit Jain 的回答非常相似,但包含了一些为什么需要“额外”步骤的原因。

java.util.regex 实现

该行:

Pattern p = Pattern.compile("foo=(?<foo>[0-9]*),bar=(?<bar>[0-9]*)");

导致分配一个新的 Pattern 对象,它在内部存储一个表示 RE 的结构 - 诸如字符选择、组、序列、贪婪与非贪婪、重复等信息。

这种模式是无状态和不可变的,因此它可以被重用,是多头的并且优化得很好。

这些行:

String s = "foo=23,bar=42";
Matcher matcher = p.matcher(s);

返回一个新Matcher对象PatternString- 一个尚未读取字符串的对象。Matcher实际上只是一个状态机的状态,其中状态机是Pattern.

可以使用以下 API 通过匹配过程步进状态机来运行匹配:

  • lookingAt(): 尝试匹配输入序列,从头开始,与模式匹配
  • find():扫描输入序列,寻找与模式匹配的下一个子序列。

在这两种情况下,都可以使用 、 和 方法读取start()中间end()状态group()

这种方法的好处

为什么有人想要逐步完成解析?

  1. 从量化大于 1 的组中获取值(即重复并最终匹配不止一次的组)。例如,在下面解析变量赋值的琐碎 RE 中:

    Pattern p = new Pattern("([a-z]=([0-9]+);)+");
    Matcher m = p.matcher("a=1;b=2;x=3;");
    m.matches();
    System.out.println(m.group(2)); // Only matches value for x ('3') - not the other values
    

    请参阅“组和捕获”JavaDoc on Pattern中的“组名称”部分

  2. 开发人员可以将 RE 用作词法分析器,开发人员可以将词法标记绑定到解析器。实际上,这适用于简单的域语言,但正则表达式可能不是成熟的计算机语言的方式。编辑这部分与前面的原因有关,但是创建处理文本的分析树通常比首先对所有输入进行词法分析更容易和更有效。
  3. (对于勇敢的人)您可以调试 RE 并找出哪个子序列无法匹配(或不正确匹配)。

但是,在大多数情况下,您不需要通过匹配来步进状态机,因此有一个方便的方法 ( matches) 可以运行模式匹配直到完成。

于 2012-10-22T01:39:19.530 回答
4

如果匹配器会自动匹配输入字符串,那么如果您希望找到模式,那将是浪费精力。

匹配器可用于检查输入字符串中的模式是否matches()为输入字符串,并且可用于find()输入字符串中的模式(甚至重复查找所有匹配的子字符串)。在您调用这两种方法之一之前,匹配器不知道您要执行什么测试,因此它无法为您提供任何匹配的组。即使您确实调用了其中一种方法,调用也可能会失败——找不到模式——在这种情况下,调用也group必须失败。

于 2012-10-20T08:17:08.900 回答
2

这是预期的并记录在案。

原因是.matches()返回一个布尔值,指示是否存在匹配项。如果有匹配,那么您可以.group(...)有意义地调用。否则,如果没有匹配,调用 to.group(...)是没有意义的。因此,您不应该在调用.group(...)之前被允许调用matches()

使用匹配器的正确方法如下:

Matcher m = p.matcher(s);
if (m.matches()) {
  ...println(matcher.group("foo"));
  ...
}
于 2012-10-16T09:30:47.637 回答
2

我的猜测是设计决策是基于查询具有清晰、定义明确的语义,不会将存在与匹配属性混为一谈。

考虑一下:如果匹配器没有成功匹配某些东西,你期望匹配器查询返回什么?

我们先考虑一下group()。如果我们没有成功匹配某些东西,Matcher 不应该返回空字符串,因为它没有匹配空字符串。我们可以null在这一点上返回。

好的,现在让我们考虑start()end()。每次返回int。在这种情况下什么int值是有效的?当然没有正数。什么负数是合适的?-1?

考虑到这一切,用户仍然必须检查每个查询的返回值,以验证是否发生匹配。或者,您可以检查它是否完全匹配成功,如果成功,则查询语义都具有明确定义的含义。如果不是,无论查询哪个角度,用户都会得到一致的行为。

我承认重用IllegalStateException可能不会导致错误情况的最佳描述。但是,如果我们要重命名/子类IllegalStateExceptionNoSuccessfulMatchException,则应该能够理解当前设计如何强制查询一致性并鼓励用户使用具有在询问时已定义的语义的查询。

TL;DR:询问生物体死亡的具体原因有什么价值?

于 2012-10-19T18:43:54.017 回答
1

您需要检查matcher.matches(). true找到匹配时它将返回,false否则。

if (matcher.matches()) {
    System.out.println(matcher.group("foo"));
    System.out.println(matcher.group("bar"));
}

如果matcher.matches()没有找到匹配项并且您调用matcher.group(...),您仍然会得到一个IllegalStateException. 这正是文档所说的:

匹配器的显式状态最初是未定义的;在成功匹配之前尝试查询它的任何部分将导致抛出 IllegalStateException。

matcher.match()返回时,false没有找到成功的匹配项,并且通过调用 example 来获取有关匹配项的信息没有多大意义group()

于 2012-10-16T09:31:06.153 回答