为什么在Java 8中split有时会在结果数组开始时删除空string?

在Java 8之前 ,我们在空string上分割就像

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

拆分机制会在标有|地方拆分

 |a|b|c| 

因为在每个字符之前和之后存在空格"" 。 因此,它会首先产生这个数组

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

后来将删除尾随的空string (因为我们没有明确提供负值来limit参数),所以最终会返回

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

在Java 8分裂机制似乎已经改变。 现在当我们使用

 "abc".split("") 

我们将得到["a", "b", "c"]数组而不是["", "a", "b", "c"]所以它看起来像空string在开始时也被删除。 但是这个理论因为例如失败了

 "abc".split("a") 

在开始["", "bc"]返回数组为空string。

有人可以解释一下这里发生了什么,以及在Java 8中如何改变这种情况的规则?

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

文档

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

如果在input序列的开始处存在正宽度匹配,则在结果数组的开头会包含一个空的前导子string。 在开始处的零宽度匹配从不产生这样的空领先子string。

在Java 8中 ,与Java 7相比,同样的子句也被添加到String.split中。

参考实现

让我们比较Java 7和Java 8中参考实现的Pattern.split的代码。代码从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中添加以下代码将排除inputstring开始处的零长度匹配,这解释了上述行为。

  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及以上版本的行为

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

  1. 如果你的正则expression式匹配零长度的string,只需在正则expression式的末尾添加(?!\A) ,并将原始正则expression式包装在非捕获组(?:...) (如有必要)。
  2. 如果你的正则expression式不能匹配零长度的string,你不需要做任何事情。
  3. 如果您不知道正则expression式是否可以匹配零长度的string,请在第1步中执行这两个操作。

(?!\A)检查string是否不在string的开始处结束,这意味着匹配在string的开始处是空的匹配。

遵循Java 7和之前的行为

没有通用的解决scheme来使split与Java 7和之前的版本向后兼容,而不是将所有的split实例replace为指向你自己的自定义实现。

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

如果在该string的开始处存在正宽度匹配,则在结果数组的开始处将包含一个空的前导子string。 在开始处的零宽度匹配从不产生这样的空领先子string。

"abc".split("")你在开始处得到一个零宽度的匹配,所以前导的空子string不包含在结果数组中。

然而,在你的第二个片段中,当你分割"a"你得到了一个正匹配的宽度(在这种情况下为1),所以如预期的那样包含空的前导子string。

(删除不相关的源代码)

split()从Java 7到Java 8的文档略有变化。具体而言,添加了以下语句:

如果在该string的开始处存在正宽度匹配,则在结果数组的开始处将包含一个空的前导子string。 在开始处的零宽度匹配从不产生这样的空领先子string。

(重点是我的)

空string拆分在开始时会生成一个零宽度匹配,所以根据上面指定的内容,在结果数组的开始处不包含空string。 相比之下,在"a"上分割的第二个例子在string的开始处会生成一个宽度匹配,因此实际上包含在结果数组的开头的空string。