注:博客内容主要摘抄自参考阅读中的两篇博文~
最近在翻阅《阿里巴巴Java开发手册》时发现了这样一条【推荐】性的原则:
// 使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分割符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险。
String str = "a,b,c,,";
String[] ary = str.split(","); // 预期大于 3,结果是 3
System.out.println(ary.length);
比较好奇产生上述结果的原因,因此决定分析一波源码。
split(String regex, int limit)
一般根据正则表达式分割字符串,limit限定分割后产生的字符串个数,超过数量限制的情况下前 limit-1 个子字符串正常分割,最后一个子字符串包含剩下的所有字符。重载方法split(String regex)
将 limit 设置为 0。
public String[] split(String regex) {
return split(regex, 0);
}
对上述方法进行追踪:
public String[] split(String regex, int limit) {
char ch = 0;
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
{
int off = 0; // 偏移量
int next = 0; // 下一次切割的地方
boolean limited = limit > 0; // 判断是否有限制,如果limit = 0则表示无限制
ArrayList<String> list = new ArrayList<>(); // 盛装切割之后的字符串
while ((next = indexOf(ch, off)) != -1) {
if (!limited || list.size() < limit - 1) {
list.add(substring(off, next));
off = next + 1;
} else { // last one
// assert (list.size() == limit - 1);
list.add(substring(off, value.length));
off = value.length;
break;
}
}
// If no match was found, return this
if (off == 0)
return new String[]{this};
// last one,but list.size() != limit - 1
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
// Construct result
int resultSize = list.size();
if (limit == 0) {
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize--;
}
}
String[] result = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}
代码并不好理解,不过别着急,我们一点点来进行剖析。
首先是一个if判断:
if (((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE))
看着很长,但实际上也就是将多个 if 判断写到了一个 if 之中:
(regex.value.length == 1 && ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1)
,分割符长度为 1 但不属于正则表达式中的元字符. $ | ( ) [ { ^ ? * + \
;regex.length() == 2 && regex.charAt(0) == '\\' && (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 && ((ch-'a')|('z'-ch)) < 0 && ((ch-'A')|('Z'-ch)) < 0)
,长度为 2,第一个字符是 ‘’(转义字符)并且第二个字符不是字母或者数字。注意这里顺便给 ch 变量进行了赋值,并且((ch-'A')|('Z'-ch)) < 0
这种判断是否为字符的方式很 nice,很独特!(ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)
,这行代码我们不予分析,有兴趣的同学可以查阅一些相关资料。了解上述代码之后,我们可以确定的一点是:在这个 if 之中,我们讨论的只是用一个字符切割字符串的情况。继续往下看,定义了几个变量,分别是:
int off = 0; // 指向每次分割的起始位置
int next = 0; // 指向分割符的下标
boolean limited = limit > 0;
ArrayList<String> list = new ArrayList<>(); // 盛装切割之后的子字符串片段
然后就是一个 while 循环:while ((next = indexOf(ch, off)) != -1)
,对 indexOf 方法进行追踪:
public int indexOf(int ch, int fromIndex) {
// 字符串长度
final int max = value.length;
// 初始化 fromIndex 大小
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
// 已没有分割符
return -1;
}
// 对 MIN_SUPPLEMENTARY_CODE_POINT 常量不做介绍
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
if (value[i] == ch) {
return i;
}
}
return -1;
} else {
return indexOfSupplementary(ch, fromIndex);
}
}
这个方法的作用是什么呢?返回分割符在字符串中相对于 fromIndex 位置后第一次出现的位置,如果没有的话就返回 -1。所以刚才的 while 循环就是当剩下的字符串还有分割符的话,就会继续循环。
剩下的就比较好理解了,但还需注意一点:
// last one,but list.size() != limit - 1
if (!limited || list.size() < limit)
list.add(substring(off, value.length));
博主特意加了list.size() != limit - 1
注释,当时在分析这一行代码时,总觉得好像有点多余,其实这是对输入的 limit 大于分割后产生的子字符串数组这一情况的补充,while 循环中的代码实际上只能正确的处理输入的limit <= 分割后产生的子字符串数组长度
这一情况。
至于在前言中提到的那个问题,相信大家已经找寻到了答案,我就不再进行赘述。
split() 方法在非特殊情况情况下是调用java.util.regex
目录下主要的两个类 Pattern 和 Matcher 进行分割处理的。String 中涉及正则匹配都是通过这两个类实现的。
Pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共(私有)构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共(默认)构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。对正则匹配在 Java 中的应用如果不熟悉的话,请自行查阅相关资料。
我们对Pattern.compile(regex).split(this, limit);
方法进行追踪:
public String[] split(CharSequence input, int limit) {
int index = 0;
boolean matchLimited = limit > 0;
ArrayList<String> matchList = new ArrayList<>();
// 获得 Matcher 对象
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()) {
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);
}
使用正则表达式对字符串进行分割的思想与普通的分割方式并没有什么区别,我也就不再进行赘述了。
使用String.split()
进行字符串分割时需要注意的点其实挺多的,网上有许多相关资料可以进行查阅,并且还有一些很经典的题目,有些还涉及到了对源码进行修改。这篇博客也只是浅尝辄止的对 split 方法进行了分析,并且正则表达式这一重点内容几乎完全没有进行讲解。如果你有时间,对正则表达式有兴趣,强烈建议要好好学习正则表达式哦~
[String源码]split()源码分析
split() 函数解析 (一)