问题
在Java 8之前当我们拆分空字符串之类的时候
String[] tokens = "abc".split("");
拆分机制将在标记为|
的地方拆分
|a|b|c|
因为空格""
存在于每个字符之前和之后。因此,它最初将生成此数组
["", "a", "b", "c", ""]
后来将是remove trailing empty strings(因为我们没有明确提供负值到limit
),所以它最终会返回
["", "a", "b", "c"]
**在Java 8中,**split机制似乎已经改变。现在我们使用的时候
"abc".split("")
我们将得到["a", "b", "c"]
array而不是["", "a", "b", "c"]
,因此它看起来像开始时的空字符串也被删除。但是这个理论失败了,例如
"abc".split("a")
在start["", "bc"]
返回带有空字符串的数组。
有人可以解释这里发生了什么,以及Java 8中这种情况的拆分规则是如何改变的?
#1 热门回答(77 赞)
在Java 7和Java 8之间,String.split
(调用Pattern.split
)的行为发生了变化。
#文档
比较Pattern.split
inJava 7和Java 8的文档,我们观察到添加了以下条款:
如果在输入序列的开头存在正宽度匹配,则在结果数组的开头包含空的前导子字符串。然而,开头的零宽度匹配从不会产生这样的空前导子串。
同样的条款也被添加到String.split
inJava 8,相比之下,Java 7。
#参考实施
让我们比较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中添加以下代码排除了输入字符串开头的零长度匹配,这解释了上面的行为。
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中的行为兼容:
- 如果你的正则表达式可以匹配零长度字符串,只需在正则表达式的末尾添加(?!\ A)并将原始正则表达式包装在非捕获组(?:...)中(如果需要)。
- 如果正则表达式与零长度字符串不匹配,则无需执行任何操作。
- 如果你不知道正则表达式是否可以匹配零长度字符串,请执行步骤1中的两个操作。
(?!\A)
检查字符串不在字符串的开头结束,这意味着匹配是字符串开头的空匹配。
##遵循Java 7及之前的行为
makesplit
与Java 7及之前版本兼容,没有通用解决方案,只需将所有实例的split
替换为你自己的自定义实现。
#2 热门回答(30 赞)
这已在文档split(String regex, limit)
中指定。
如果在此字符串的开头存在正宽度匹配,则在结果数组的开头包含空的前导子字符串。然而,开头的零宽度匹配从不会产生这样的空前导子串。
In"abc".split("")
你在开头有一个零宽度匹配,因此前导空子字符串不包含在结果数组中。
但是在你的第二个片段中,当你拆分"a"
时,你得到了一个正宽度匹配(在这种情况下是1),所以空的前导子串被包含在预期中。
(删除了不相关的源代码)
#3 热门回答(12 赞)
split()
从Java 7到Java 8的文档略有变化。具体来说,添加了以下语句:
如果在此字符串的开头存在正宽度匹配,则在结果数组的开头包含空的前导子字符串。然而,开头的零宽度匹配从不会产生这样的空前导子串。
(强调我的)
空字符串拆分在开头生成零宽度匹配,因此根据上面指定的内容,在结果数组的开头不包括空字符串。相比之下,分裂on"a"
的第二个示例在字符串的开头生成了一个正宽度匹配,因此实际上在结果数组的开头包含一个空字符串。