为什么在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中的行为兼容:
- 如果你的正则expression式匹配零长度的string,只需在正则expression式的末尾添加
(?!\A)
,并将原始正则expression式包装在非捕获组(?:...)
(如有必要)。 - 如果你的正则expression式不能匹配零长度的string,你不需要做任何事情。
- 如果您不知道正则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。