Java String.split() 有时给出空白字符串

2022-09-04 20:10:37

我正在制作一个基于文本的骰子滚轮。它接收像“2d10+5”这样的字符串,并返回一个字符串作为滚动的结果。我的问题出现在分词器中,它将字符串拆分为有用的部分,以便我解析为信息。

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

这会产生奇怪的、意想不到的结果。我不知道究竟是什么原因导致了它们。它可能是正则表达式,我的误解,或者Java只是Java。以下是正在发生的事情:

  • 3d6+4生成字符串数组 。这是正确的。[3, d6, +4]
  • d%生成字符串数组 。这是正确的。[d%]
  • d20生成字符串数组 。这是正确的。[d20]
  • d%+3生成字符串数组 。这是不正确的。[, d%, +3]
  • d20+2生成字符串数组 。这是不正确的。[, d20, +2]

在第四个和第五个示例中,一些奇怪的事情导致数组前面出现一个额外的空字符串。这并不是说字符串前面缺少数字,因为其他例子反驳了这一点。它不是百分号的存在,也不是加号的存在。

现在,我只是继续通过空白字符串上的for循环,但这感觉有点像创可贴解决方案。有没有人知道是什么原因导致数组前面的空白字符串?我该如何修复它?


答案 1

通过挖掘源代码,我得到了这种行为背后的确切问题。

该方法在内部使用 。返回结果数组之前的 split 方法会检查最后一个匹配的索引,或者是否确实存在匹配项。如果最后匹配的索引是 ,则表示您的模式仅在字符串开头匹配一个空字符串或根本不匹配,在这种情况下,返回的数组是包含相同元素的单元素数组。String.split()Pattern.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

如果上述代码中的最后一个条件 - 为 true,则返回单元素数组和输入字符串。index == 0

现在,考虑可以 .index0

  1. 当根本没有匹配时。(如上面的评论中所述)
  2. 如果在开头找到匹配项,并且匹配字符串的长度为 ,则块中索引的值(循环内)-0ifwhile

    index = m.end();
    

    将为 0。唯一可能的匹配字符串是空字符串(长度 = 0)。这里的情况正是如此。而且不应该有任何进一步的匹配,否则将更新为不同的索引。index

因此,考虑到您的情况:

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

  • 因为将有两个匹配项,一个在 之前,一个在 之前。因此,索引值将被更新,因此将返回上述代码中的,其中包含空字符串,这是分隔符拆分的结果,分隔符是字符串的第一个字符,如@Stema的答案中已经解释的那样。d20+2d+ArrayList

因此,要获得所需的行为(仅当不在开头时才在分隔符上进行拆分),您可以在正则表达式模式中添加一个否定的后看):

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

这将在空字符串后跟字符类上进行拆分,但不以字符串的开头开头进行拆分。


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

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

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


答案 2

我很惊讶它不会发生在案例2和3中,所以这里真正的问题是

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

正如Rohit Jain在他的详细分析中解释的那样,当在字符串的开头只找到一个匹配项并且match.end索引为0时,就会发生这种情况。(这只会发生在仅使用查找断言来查找匹配项时)。

问题是,这从你正在分裂的字符开始。因此,您的正则表达式在第一个字符之前匹配,并且您在开始时得到一个空字符串。d%+3

您可以添加一个 lookbehind,以确保您的表达式在字符串的开头不匹配,以便它不会在那里拆分:

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

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