Java基础: String及其相关类
AbstractStringBuilder
AbstractStringBuilder是一个抽象类,StringBuilder和StringBuffer都直接继承此类。
内部有两个属性值:
char[] value; // 存储数据
int count; // 数据的大小
数据存储在char数组中,数据存储范围是0~count。其数组扩容原理是原来数组长度*2+2,与传入的长度参数作比较,取两者的最大值。任何修改操作都是维护char数组和count值,原理都大同小异。下面重点讲reverse
反转方法:
public AbstractStringBuilder reverse() {
boolean hasSurrogates = false;
int n = count - 1;
for (int j = (n-1) >> 1; j >= 0; j--) {
int k = n - j;
char cj = value[j];
char ck = value[k];
value[j] = ck;
value[k] = cj;
if (Character.isSurrogate(cj) ||
Character.isSurrogate(ck)) {
hasSurrogates = true;
}
}
if (hasSurrogates) {
reverseAllValidSurrogatePairs();
}
return this;
}
private void reverseAllValidSurrogatePairs() {
for (int i = 0; i < count - 1; i++) {
char c2 = value[i];
if (Character.isLowSurrogate(c2)) {
char c1 = value[i + 1];
if (Character.isHighSurrogate(c1)) {
value[i++] = c1;
value[i] = c2;
}
}
}
}
第一行的hasSurrogates
是什么意思呢。这里要先讲一下字符的Surrogates(代理)。
现实世界里的所有字符都可以用Unicode编码来表示,但要让计算机能存储,就要转换成二进制形式,叫做Unicode转换格式(Unicode Transformation Formats),即UTF。UTF有多种编码,有UTF-8/UTF-16/UTF-32,Java内部的字符编码为UTF-16,一般用2个字节(即16bit)来存储,那么就有2^16=65536个值。
但这个值远小于Unicode的数量,怎么办呢?
UTF-16编码范围为\u0000~\uFFFF
,其中规定\uD800~\uDBFF
为High Surrogates(高代理),\uDC00~\uDFFF
为Low Surrogates(低代理),各有1024个,两者成对出现,高代理在前,低代理在后,组成一个SurrogatePair来表示一个Unicode码,这样就多出来了1024*1024个字符。详情可参考Character类,里面也有判断是否是High Surrogates/Low Surrogates/Surrogates/SurrogatePair的方法。
现在再来看reverse
方法,for循环就是从中间索引 j
开始,交换(count-1-j)
索引的值,随着j--
,直到 j==0
,整个char数组就都反转了。但是如果存在Surrogates,就变成了低代理在前,高代理在后了,不是一个Unicode码了。如果存在这种情况就调用reverseAllValidSurrogatePairs
方法,反转SurrogatePair。for循环里如果当前字符为Low Surrogates,下一个字符为High Surrogates,就调换。调换代码转换成这样更清晰易懂。
value[i] = c1;
i++;
value[i] = c2;
StringBuilder/StringBuffer
这两个类一起说,都是继承AbstractStringBuilder
类,默认初始容量都是16,里面的大部分方法也都是直接调用父类的方法,不同之处是StringBuffer
的大部分方法加了synchronized
,是线程安全的,里面也多了个属性char[] toStringCache
,缓存父类value的0~count的数据,但任何修改操作都会将其置为null
。推荐使用StringBuilder
,因为效率更高。
StringBuilder sb = new StringBuilder();
sb.append("a").append(1).append('-').append(true);
sb.toString(); // a1-true
StringJoiner
StringJoiner
用于构造由分隔符分割,包含前缀和后缀的字符序列。
StringJoiner sj = new StringJoiner(",", "{", "}");
sj.add("George").add("Sally").add("Fred");
sj.toString(); // {George,Sally,Fred}
StringJoiner
内部维护了一个StringBuilder value
属性,数据格式为前缀+元素+分隔符形式,如 {George,Sally,Fred
,方便添加元素。只有调用toString()
方法时 value
才追加后缀获取到String
,之后再移除value
的后缀,恢复到原来的格式。
String
String类方法有很多,我只讲我之前不熟的,呵呵。
- join方法
public static String join(CharSequence delimiter, CharSequence... elements) {
StringJoiner joiner = new StringJoiner(delimiter);
for (CharSequence cs: elements) {
joiner.add(cs);
}
return joiner.toString();
}
这没啥说的,StringJoiner
的构造函数只传分隔符的话,默认前后缀都是""
,看例子:
String message = String.join("-", "Java", "is", "cool");
// "Java-is-cool"
- format方法
public static String format(String format, Object... args) {
return new Formatter().format(format, args).toString();
}
String格式化输出方法,直接调用的java.util.Formatter
的方法,这个类以后再说,先简单举例:
String.format("%s, %d, %f", "Fred", 1, 1.0);
// Fred, 1, 1.000000
- split方法
public String[] split(String regex, int limit) {
/* fastpath if the regex is a
(1)one-char String and this character is not one of the
RegEx's meta characters ".$|()[{^?*+\\", or
(2)two-char String and the first char is the backslash and
the second is not the ascii digit or ascii letter.
*/
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; // indexOf方法的偏移量
int next = 0; // 符合条件的索引
boolean limited = 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};
// Add remaining segment
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);
}
public String[] split(String regex) {
return split(regex, 0);
}
split
方法可以根据regex
将String
分割成一个String数组,如果limit
大于0,就分割成指定的大小。假设 String str = "boo:and:foo"
:
regex | limit | result |
---|---|---|
: | 2 | { “boo”, “and:foo” } |
: | 5 | { “boo”, “and”, “foo” } |
: | -2 | { “boo”, “and”, “foo” } |
再来看源码,上来有个if判断,如果不符合条件就调用正则表达式类
java.util.regex.Pattern
来分割字符串,这个现在先不管。符合if条件有几中情况:1. 当regex
只有一个字符,并且不能是 .$|()[{^?*+\\
其中任何一个(\\
是反斜杠的转义,就是\
);2. 由两个字符组成,第一个必须是\\
,第二个不能是数字或字母;3. 前两个条件符合一个即可,若符合条件1,则第一个字符不属于字符高/低代理的范围,若符合条件2,则第二个字符不属于字符高/低代理的范围。
ch
在if
条件里就赋值了,regex
只有一个字符,ch = regex.charAt(0)
;只有两个字符,ch = regex.charAt(1)
。第一个while循环里,从0开始查找字符串里等于ch
的字符,找到就截取0~next的字符串,放到list
中,然后继续从next
下一个字符(偏移量off = next + 1;
)继续查找,直到 next = -1
跳出循环,或者list的大小等于limit-1
,把剩余字符串都放到list
中,如表格中 limit=2
的情况。
如果字符串中没有等于ch
的字符,就返回只包含整个字符串的数组。
如果没用限制数组大小或者限定的数组大小太大,如表格中 limit=-2
或者limit=5
的情况,就把字符串剩余的部分添加进去。因为这两种情况下,while循环里的else语句根本没有执行。
最后是将list
转换为String
数组,但是我没看懂为什么limit=0
时,不返回list
中最后的元素为空的数据,这就导致了下面的两种情况:
String str = "a:b:c::";
str.split(":"); // [a, b, c]
str.split(":", 10); // [a, b, c, , ]
看源码时发现一个问题,就是regex
是 \\|
的时候,匹配的字符第二个字符|
,而不是\\|
,这就导致了问题,如果我们想匹配的就是\\|
,而不是转移后的|
呢。看下面的两个例子就知道了:
String s1 = "a\\|b|c\\|d";
s1.split("\\|"); // 不走正则,返回错误结果:[a\, b, c\, d]
s1.split("\\\\\\|"); // 走正则,返回预期结果:[a, b|c, d]
// "\\\\\\|"转义后是"\\|"
我们想匹配的是\\|
,但第一种方法只匹配了|
,得出了错误结果。注意a\
只是a\\
转义后的结果。
String s2 = "a|b|c";
s2.split("\\|"); // 不走正则,返回预期结果:[a, b, c]
s2.split("|"); // 走正则,返回错误结果:[a, |, b, |, c]
// "|"在正则表达式中代表"或"的意思
这次我们想匹配 |
,参数就要写\\|
了。
调用整个方法,稍不注意就会出现不符合预期的结果,要多注意。看这个方法最大的收获就是知道了 String
的 split()
方法什么时候走的正则,什么时候不是走的正则。
先这样吧,以后再碰见String相关类再补充。