为什么在Java 8中split有时会在结果数组的开头删除空字符串?


110

在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")

在start处返回带有空字符串的数组["", "bc"]

有人可以解释一下这是怎么回事,以及拆分规则在Java 8中是如何变化的吗?


Java8似乎可以解决该问题。同时,s.split("(?!^)")似乎可行。
shkschneider 2014年

2
@shkschneider我的问题中描述的行为不是Java-8之前版本的错误。这种行为不是特别有用,但是仍然是正确的(如我的问题所示),因此我们不能说它是“固定的”。我认为它更像是改进,因此我们可以使用split("")cryptic(对于不使用regex的人)split("(?!^)")split("(?<!^)")其他少数regex代替。
Pshemo

1
在将fedora升级到Fedora 21之后,遇到同样的问题,fedora 21随JDK 1.8一起提供,因此我的IRC游戏应用程序已损坏。
刘岩刘研2014年

7
这个问题似乎是Java 8中这一重大更改的唯一文档。Oracle将其排除在不兼容列表之外
肖恩·范·哥德

4
JDK中的这一更改仅使我花了2个小时来查找问题所在。该代码在我的计算机(JDK8)上运行良好,但在另一台计算机(JDK7)上却神秘地失败了。Oracle 真的应该更新String.split(String regex)的文档,而不是Pattern.split或String.split(String regex,int limit),因为这是迄今为止最常见的用法。Java因其可移植性(又称为WORA)而闻名。这是一项重大的重大突破,完全没有记录在案。
PoweredByRice 2015年

Answers:


84

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

文献资料

Pattern.splitJava 7Java 8的文档之间进行比较,我们观察到添加了以下子句:

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

Java 7相比String.splitJava 8中还添加了相同的子句。

参考实施

让我们比较一下Pattern.splitJava 7和Java 8中的参考实现的代码。该代码是从grepcode中检索的,用于版本7u40-b43和8-b132。

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);
}

Java 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及更高版本中的以下行为

为了使make split在各个版本中的行为保持一致并与Java 8中的行为兼容:

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

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

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

没有通用的解决方案可以使它split与Java 7及更低版本向后兼容,除非替换所有实例split以指向您自己的自定义实现。


知道如何更改split("")代码以使其在不同的Java版本之间保持一致吗?
丹尼尔(Daniel)

2
@Daniel:可以通过添加(?!^)到正则表达式的末尾并将其原始正则表达式包装在非捕获组中(?:...)(如有必要)来使其向前兼容(遵循Java 8的行为),但是我想不到任何使它向后兼容的方法(遵循Java 7和以前的旧行为)。
nhahtdh

感谢您的解释。你能描述一下"(?!^)"一下吗?在什么情况下它将不同于""?(我在regex上很糟糕!:-/)。
丹尼尔(Daniel)

1
@Daniel:其含义受Pattern.MULTILINE标志影响,而\A无论标志如何,始终在字符串的开头匹配。
nhahtdh

30

这已在的文档中指定split(String regex, limit)

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

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

但是,在拆分的第二个代码段中,"a"您得到了正宽度匹配(在这种情况下为1),因此按预期包括了空的前导子字符串。

(删除了无关的源代码)


3
这只是一个问题。可以从JDK中发布一段代码吗?还记得Google-Harry Potter-Oracle的版权问题吗?
Paul Vargas 2014年

6
@PaulVargas公平地说,我不知道,但我认为可以,因为您可以下载JDK,并解压缩包含所有源代码的src文件。因此,从技术上讲,每个人都可以看到源。
Alexis C.

12
@PaulVargas“开源”中的“开放”确实代表着某种东西。
Marko Topolnik 2014年

2
@ZouZou:仅仅因为每个人都能看到它并不意味着您可以重新发布它
user102008 2014年

2
@Paul Vargas,IANAL,但在许多其他情况下,此类帖子属于引用/合理使用情况。更多的话题是在这里:meta.stackexchange.com/questions/12527/...
亚历Pakka

14

split()从Java 7到Java 8 的文档略有变化。特别是,添加了以下语句:

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

(强调我的)

空字符串拆分会在开头生成零宽度匹配项,因此根据上面指定的内容,在结果数组的开头不包含空字符串。相比之下,第二个拆分示例在字符串的开头"a"生成一个宽度匹配,因此实际上在结果数组的开头包含一个空字符串。


再过几秒钟就变得与众不同。
Paul Vargas 2014年

2
@PaulVargas实际上在此处arshajii在邹邹几秒钟前发布了答案,但很遗憾,邹邹在这里早些时候回答了我的问题。我想知道是否应该问这个问题,因为我已经知道了一个答案,但是这似乎很有趣,而邹邹的早期评论也应得到一定的声誉。
Pshemo

5
尽管新行为看起来更合乎逻辑,但显然这是向后兼容中断。进行此更改的唯一理由是这种"some-string".split("")情况很少见。
ivstas 2014年

4
.split("")不是不匹配任何内容的唯一分割方法。我们在jdk7中使用了正向前瞻正则表达式,该正则表达式在开始时也匹配,并产生了一个空的head元素,该元素现在消失了。github.com/spray/spray/commit/...
jrudolph
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.