Java中的String.split()有时会返回空字符串。

13
我正在制作一个基于文本的骰子 Roller。它接受像 "2d10+5" 这样的字符串,并返回其掷骰结果的字符串。我的问题出现在将字符串拆分为有用部分以便我解析信息的令牌化程序中。
这导致了奇怪,意外的结果。我不知道具体是什么导致了这些结果。可能是正则表达式、我的误解,或者是 Java 的问题。以下是发生的情况:
- "3d6+4" 产生了字符串数组 "[3, d6, +4]"。这是正确的。 - "d%" 产生了字符串数组 "[d%]"。这是正确的。 - "d20" 产生了字符串数组 "[d20]"。这是正确的。 - "d%+3" 产生了字符串数组 "[, d%, +3]"。这是不正确的。 - "d20+2" 产生了字符串数组 "[, d20, +2]"。这是不正确的。
在第四和第五个示例中,某些奇怪的事情会导致额外的空字符串出现在数组的开头。这不是字符串前面缺少数字,因为其他示例证明了这一点。这也不是百分比符号或加号的存在所引起的问题。
目前,我只是在空字符串上继续 for 循环,但这感觉像是一个权宜之计。是否有人知道是什么导致了数组开头的空字符串?我该如何修复它?

这种行为已经不再存在了。我相信它在Java 8中被改变了。在javadoc中有一个新的段落,它说:“然而,在开头的零宽匹配永远不会产生这样的空前缀子字符串。” - ajb
3个回答

13

我仔细查看了源代码,找到了导致这种行为的确切问题。

String.split() 方法在内部使用 Pattern.split()。split 方法在返回结果数组之前会检查最后匹配的索引是否存在或者是否有实际匹配。如果最后匹配的索引是 0,那么意味着你的模式只匹配了空字符串开头或者根本没有匹配,在这种情况下,返回的数组将是一个包含相同元素的单个元素数组。

以下是源代码:

public String[] split(CharSequence input, int limit) {
        int index = 0;
        boolean matchLimited = limit > 0;
        ArrayList<String> matchList = new ArrayList<String>();
        Matcher m = matcher(input);

        // Add segments before each match found
        while(m.find()) {
            if (!matchLimited || matchList.size() < limit - 1) {
                String match = input.subSequence(index, m.start()).toString();
                matchList.add(match);

                // Consider this assignment. For a single empty string match
                // m.end() will be 0, and hence index will also be 0
                index = m.end();
            } else if (matchList.size() == limit - 1) { // last one
                String match = input.subSequence(index,
                                                 input.length()).toString();
                matchList.add(match);
                index = m.end();
            }
        }

        // If no match was found, return this
        if (index == 0)
            return new String[] {input.toString()};

        // Rest of them is not required

如果上述代码中的最后一个条件 - index == 0,为真,则返回带有输入字符串的单个元素数组。
现在,考虑当index可以是0的情况。
  1. When there is no match at all. (As already in the comment above that condition)
  2. If the match is found at the beginning, and the length of matched string is 0, then the value of index in the if block (inside the while loop) -

    index = m.end();
    

    will be 0. The only possible match string is an empty string (length = 0). Which is exactly the case here. And also there shouldn't be any further matches, else index would be updated to a different index.

考虑你的情况:

  • 对于d%,模式只有一个匹配项,在第一个d之前。因此,索引值将为0。但由于没有进一步的匹配,索引值不会更新,if条件成立,并返回带有原始字符串的单个元素数组。

  • 对于d20+2,将有两个匹配项,一个在d之前,另一个在+之前。所以索引值将被更新,因此上面代码中的ArrayList将被返回,其中包含根据分隔符(即字符串的第一个字符)拆分而得到的空字符串,正如@Stema的答案中已经解释过的那样。

因此,要获得您想要的行为(即仅在分隔符不在开头时拆分),可以在您的正则表达式模式中添加负向回溯:

"(?<!^)(?=[dk+-])"  // You don't need to escape + and hyphen(when at the end)

这将在空字符串后跟您的字符类上拆分,但不在字符串开头之前。


考虑在正则表达式模式"a(?=[dk+-])"上拆分字符串"ad%"的情况。这将给您一个数组,其中第一个元素为空字符串。唯一的变化是,空字符串被替换为a

"ad%".split("a(?=[dk+-])");  // Prints - `[, d%]`

为什么?因为匹配字符串的长度是1。所以第一次匹配后的索引值 - m.end()不会是0,而是1,因此单个元素数组将不会被返回。


哇,这是一份详细的分析,感谢解释。我只是认识到这取决于比赛的数量。 - stema
@stema。其实,这是一个很好的问题,由OP提出。我从来没有想过这个问题。今天学到了新东西 :) - Rohit Jain
哇,谢谢!这完全解释清楚了。非常好的答案。既解决了我的问题,又帮助我学习了。 - Corey Noel
我们可以称之为一个 bug 吗? - assylias
1
@assylias。可能吧。但我猜这是最罕见的情况,直到现在都找不到。但由于源代码没有以意外的方式运行,所以这不是这种情况。但我同意应该在Javadoc中提到它。我不确定是否应该将其发布为错误。 - Rohit Jain
文档非常清楚地说明了默认情况下如何删除尾部的空标记,并提供了一种机制来覆盖该行为。然而,没有提到如何处理前导空标记,也没有改变这种行为的方法。再加上包含不同数量标记的输入的不一致行为,那肯定是一个错误。 - Alan Moore

5

对于第二种和第三种情况,我很惊讶它没有发生,因此真正的问题是

为什么在“d20”和“d%”开头没有空字符串?

正如Rohit Jain在他的详细分析中解释的那样,当字符串起始处只有一个匹配项且该匹配项的match.end索引为0时,就会出现这种情况。(只有使用环视断言找到匹配项时,才可能发生这种情况)。

问题在于,d%+3以您正在拆分的字符开头。 因此,您的正则表达式会在第一个字符之前进行匹配,并在开头得到一个空字符串。

您可以添加一个后顾断言,以确保您的表达式不会在字符串开头匹配,从而避免在那里分割:

String[] tokens = message.split("(?<!^)(?=[dk\\+\\-])");

(?<!^)是一个回顾断言,当它不在字符串的开头时为真。


1
+1 因为你的解决方案有效 - 但是 d20 也以一个字符开头,而操作者正在分割该字符,因此你给出的原因可能不正确。 - assylias
好的,这些测试字符串不应该产生不同的结果,我需要启动Eclipse并检查。 - stema
好的答案对于“lookbehind”,我没有想到。 - Maxim Shoustin
2
你的解决方案成功了。在前面加上(?<!^)解决了问题,尽管我仍然不完全确定为什么有时会出现,而有时则不会。 - Corey Noel
@CoreyNoel,我刚刚测试了一下。我可以确认你的结果,但我不知道为什么会这样。 - stema
显示剩余2条评论

0
我建议使用简单匹配而不是分割:
Matcher matcher = Pattern.compile("([1-9]*)(d[0-9%]+)([+-][0-9]+)?").matcher(string);
if(matcher.matches()) {
    String first = matcher.group(1);
    // etc
}

正则表达式没有保证,但我认为它会起作用...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接