为什么在 Java 8 中,拆分有时会在结果数组的开头删除空字符串?文档参考实现保持兼容性

2022-08-31 09:22:30

在Java 8之前,当我们在空字符串上拆分时,例如

String[] tokens = "abc".split("");

拆分机制将在标有|

|a|b|c|

因为每个字符之前和之后都存在空白区域。因此,它将首先生成此数组""

["", "a", "b", "c", ""]

稍后将删除尾随的空字符串(因为我们没有显式为参数提供负值),因此它最终将返回limit

["", "a", "b", "c"]

在Java 8中,拆分机制似乎已经改变。现在当我们使用时

"abc".split("")

我们将得到数组而不是 。["a", "b", "c"]["", "a", "b", "c"]

我的第一个猜测是,也许现在前导空字符串也会被删除,就像尾随的空字符串一样。

但是这个理论失败了,因为

"abc".split("a")

返回 ,因此未删除前导空字符串。["", "bc"]

有人可以解释一下这里发生了什么吗?在 Java 8 中,规则发生了哪些变化?split


答案 1

(调用 )的行为在 Java 7 和 Java 8 之间变化。String.splitPattern.split

文档

比较 Java 7 和 Java 8 中的文档,我们观察到添加了以下子句:Pattern.split

当输入序列的开头存在正宽度匹配时,则在结果数组的开头包含一个空的前导子字符串。但是,开头的零宽度匹配永远不会产生这样的空前导子字符串。

与Java 7相比,Java 8中也添加了相同的子句。String.split

参考实现

让我们比较 Java 7 和 Java 8 中引用实现的代码。该代码是从版本 7u40-b43 和 8-b132 的 grepcode 中检索的。Pattern.split

Java 7

public String[] split(CharSequence input, int limit) {
    int index = 0;
    boolean matchLimited = limit > 0;
    ArrayList<String> matchList = new ArrayList<>();
    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);
            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()};

    // Add remaining segment
    if (!matchLimited || matchList.size() < limit)
        matchList.add(input.subSequence(index, input.length()).toString());

    // Construct result
    int resultSize = matchList.size();
    if (limit == 0)
        while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
            resultSize--;
    String[] result = new String[resultSize];
    return matchList.subList(0, resultSize).toArray(result);
}

爪哇 8

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

    // Add segments before each match found
    while(m.find()) {
        if (!matchLimited || matchList.size() < limit - 1) {
            if (index == 0 && index == m.start() && m.start() == m.end()) {
                // no empty leading substring included for zero-width match
                // at the beginning of the input char sequence.
                continue;
            }
            String match = input.subSequence(index, m.start()).toString();
            matchList.add(match);
            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()};

    // Add remaining segment
    if (!matchLimited || matchList.size() < limit)
        matchList.add(input.subSequence(index, input.length()).toString());

    // Construct result
    int resultSize = matchList.size();
    if (limit == 0)
        while (resultSize > 0 && matchList.get(resultSize-1).equals(""))
            resultSize--;
    String[] result = new String[resultSize];
    return matchList.subList(0, resultSize).toArray(result);
}

在 Java 8 中添加以下代码将排除输入字符串开头的零长度匹配,这解释了上述行为。

            if (index == 0 && index == m.start() && m.start() == m.end()) {
                // no empty leading substring included for zero-width match
                // at the beginning of the input char sequence.
                continue;
            }

保持兼容性

Java 8 及更高版本中的以下行为

要使行为在各个版本之间保持一致并与 Java 8 中的行为兼容,请执行以下操作:split

  1. 如果您的正则表达式可以匹配零长度字符串,只需在正则表达式的末尾添加,并将原始正则表达式包装在非捕获组中(如有必要)。(?!\A)(?:...)
  2. 如果您的正则表达式无法匹配零长度字符串,则无需执行任何操作。
  3. 如果您不知道正则表达式是否可以匹配零长度字符串,请执行步骤 1 中的两个操作。

(?!\A)检查字符串是否不在字符串的开头结束,这意味着匹配项在字符串的开头是空匹配项。

Java 7 及更早版本中的以下行为

没有通用的解决方案可以使 Java 7 及更早版本向后兼容,除了替换指向您自己的自定义实现的所有实例之外。splitsplit


答案 2

这已在 split(字符串正则表达式,限制)的文档中指定。

当此字符串的开头存在正宽度匹配时,则在结果数组的开头包含一个空的前导子字符串。但是,开头的零宽度匹配永远不会产生这样的空前导子字符串。

在开始时,您有一个零宽度匹配,因此前导空子字符串不包含在结果数组中。"abc".split("")

但是,在第二个代码段中,当您拆分时,您得到了一个正宽度匹配(在本例中为 1),因此空的前导子字符串将按预期包含在内。"a"

(删除了不相关的源代码)