概况
Java 语言使用 String 类用来代表字符串,实际上 String 对象的值是一个常量,一旦创建后不能被改变。正式因为其不可变,所以它是线程安全地,可以多个线程共享。
相信对于 String 的使用大家都再熟悉不过的了,这里就了解下 JDK 中怎么实现 String 类的。
继承结构
--java.lang.Object
--java.lang.String
复制代码
类定义
public final class String implements java.io.Serializable, Comparable<String>, CharSequence
复制代码
String 类被声明为 final,说明它不能再被继承。同时它实现了三个接口,分别为 Serializable、Comparable 和 CharSequence。其中 Serializable 接口表明其可以序列化;
InputStream 被定为 public 且 abstract 的类,实现了Closeable接口。
Closeable 接口表示 InputStream 可以被close,接口定义如下:
public interface Closeable extends AutoCloseable {
public void close() throws IOException;
}
复制代码
主要属性
private final byte[] value;
private final byte coder;
private int hash;
static final boolean COMPACT_STRINGS;
static {
COMPACT_STRINGS = true;
}
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
复制代码
- value 用于存储字符串对象的值。
- coder 表示字符串对象所用的编码器,为
LATIN1
或UTF16
。 - hash 为字符串对象的哈希值,默认值为0。
- COMPACT_STRINGS 表示是否使用紧凑的字符数组布局,默认使用。
- CASE_INSENSITIVE_ORDER 表示用于排序的比较器
内部类
该内部类主要是提供排序的比较器,实现了Comparator
接口和compare
方法,另外一个readResolve
方法用于替换反序列化时的对象。compare
核心方法的逻辑是,根据两者编码是否相同做处理,如果相同则分 Latin1 或 UTF16 两种情况比较,类似地,如果两者编码不同,则需要用 Latin1 编码与 UTF16 编码比较,而 UTF16 则要与 Latin1 比较。
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
private static final long serialVersionUID = 8575799808933029326L;
public int compare(String s1, String s2) {
byte v1[] = s1.value;
byte v2[] = s2.value;
if (s1.coder() == s2.coder()) {
return s1.isLatin1() ? StringLatin1.compareToCI(v1, v2)
: StringUTF16.compareToCI(v1, v2);
}
return s1.isLatin1() ? StringLatin1.compareToCI_UTF16(v1, v2)
: StringUTF16.compareToCI_Latin1(v1, v2);
}
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
复制代码
构造方法
有很多种构造方法,看主要的几个。没有参数的构造方法直接将空字符串的 value 和 coder 进行赋值。
public String() {
this.value = "".value;
this.coder = "".coder;
}
复制代码
类似的,传入 String 对象的构造方法则将该对象对应的 value 、coder 和 hash 进行赋值。
public String(String original) {
this.value = original.value;
this.coder = original.coder;
this.hash = original.hash;
}
复制代码
构造方法传入 char 数组时,主要逻辑就是如果 COMPACT_STRINGS 为 true,即使用紧凑布局的话,则尝试将其转换成为 LATIN1 编码(即ISO-8859-1编码),这里说尝试是因为 char 数组中可能包含了非 LATIN1 编码,此时是压缩失败的,只有数组中全部都为 LATIN1 编码时才能压缩成功。类似的还有传入 int 数组的,int 类型占用4个字节,只有全部符合转换才能转成 LATIN1 编码。
public String(char value[]) {
this(value, 0, value.length, null);
}
String(char[] value, int off, int len, Void sig) {
if (len == 0) {
this.value = "".value;
this.coder = "".coder;
return;
}
if (COMPACT_STRINGS) {
byte[] val = StringUTF16.compress(value, off, len);
if (val != null) {
this.value = val;
this.coder = LATIN1;
return;
}
}
this.coder = UTF16;
this.value = StringUTF16.toBytes(value, off, len);
}
复制代码
构造方法传入 byte 数组时,同时会传入 charsetName,即编码。核心操作为StringCoding.decode
,它会先根据编码对 byte 数组进行解码,解码过程会判断是否全部都在 LATIN1 编码内,如果是则使用 LATIN1 编码,否则使用 UTF16 编码,并且将解码后对应的 byte 数组赋值给 String 对象的 value。
public String(byte bytes[], int offset, int length, String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null)
throw new NullPointerException("charsetName");
checkBoundsOffCount(offset, length, bytes.length);
StringCoding.Result ret =
StringCoding.decode(charsetName, bytes, offset, length);
this.value = ret.value;
this.coder = ret.coder;
}
复制代码
主要方法
length方法
字符串的长度应该是字符的长度,而不是字节数组的长度,所以这里做了右移操作,LATIN1 编码时coder()
为0,字符串长度等于字节数组长度。UTF16 编码时coder()
为1,字符串等于字节数组长度一半。
public int length() {
return value.length >> coder();
}
复制代码
isEmpty方法
通过判断 byte 数组长度是否为0来判断字符串对象是否为空。
public boolean isEmpty() {
return value.length == 0;
}
复制代码
charAt方法
取字符需要根据编码来操作,如果是 LATIN1 编码,则直接取 byte 数组中对应索引的元素,并转成 char 类型即可。如果是 UTF16 编码,因为它每个 UTF16 编码占用两个字节,所以需要将索引乘以2后作为最终索引取得两个字节并转换成 char 类型,具体实现逻辑如getChar
方法所示。
public char charAt(int index) {
if (isLatin1()) {
return StringLatin1.charAt(value, index);
} else {
return StringUTF16.charAt(value, index);
}
}
static char getChar(byte[] val, int index) {
index <<= 1;
return (char)(((val[index++] & 0xff) << HI_BYTE_SHIFT) |
((val[index] & 0xff) << LO_BYTE_SHIFT));
}
复制代码
codePointAt方法
获取字符串对应索引的 Unicode 代码点,根据编码做不同处理。如果是 LATIN1 编码,直接将 byte 数组对应索引的元素与0xff
做&操作并转成 int 类型。相应的,UTF16 编码也需要对应做转换,它包含了两个字节。
public int codePointAt(int index) {
if (isLatin1()) {
checkIndex(index, value.length);
return value[index] & 0xff;
}
int length = value.length >> 1;
checkIndex(index, length);
return StringUTF16.codePointAt(value, index, length);
}
复制代码
codePointBefore方法
用于返回指定索引值前一个字符的代码点,实现与codePointAt
方法类似,只是索引值要减1。
public int codePointBefore(int index) {
int i = index - 1;
if (i < 0 || i >= length()) {
throw new StringIndexOutOfBoundsException(index);
}
if (isLatin1()) {
return (value[i] & 0xff);
}
return StringUTF16.codePointBefore(value, index);
}
复制代码
codePointCount方法
用于得到指定索引范围内代码点的个数,如果是 Latin1 编码则直接索引值相减,因为每个字节肯定都属于一个代码点。如果是 UTF16 编码则要检查是否存在 High-surrogate 代码和 Low-surrogate 代码,如果存在则说明需要4个字节来表示一个字符,此时要把 count 减1。
public int codePointCount(int beginIndex, int endIndex) {
if (beginIndex < 0 || beginIndex > endIndex ||
endIndex > length()) {
throw new IndexOutOfBoundsException();
}
if (isLatin1()) {
return endIndex - beginIndex;
}
return StringUTF16.codePointCount(value, beginIndex, endIndex);
}
private static int codePointCount(byte[] value, int beginIndex, int endIndex, boolean checked) {
assert beginIndex <= endIndex;
int count = endIndex - beginIndex;
int i = beginIndex;
if (checked && i < endIndex) {
checkBoundsBeginEnd(i, endIndex, value);
}
for (; i < endIndex - 1; ) {
if (Character.isHighSurrogate(getChar(value, i++)) &&
Character.isLowSurrogate(getChar(value, i))) {
count--;
i++;
}
}
return count;
}
public static int codePointCount(byte[] value, int beginIndex, int endIndex) {
return codePointCount(value, beginIndex, endIndex, false /* unchecked */);
}
复制代码
offsetByCodePoints方法
该方法用于返回 String 中从给定的 index 处偏移 codePointOffset 个 Unicode 代码点的索引,要注意 Unicode 代码可能两个字节也可能四个字节。逻辑为:
- index 不能小于0且不能超过。
- 当 codePointOffset 大于0时,通过 for 循环递增索引值,判断如果存在 High-surrogate 代码和 Low-surrogate 代码则索引值还需额外加1。
- 当 codePointOffset 小于0时,通过 for 循环递减索引值,判断如果存在 High-surrogate 代码和 Low-surrogate 代码则索引值还需额外减1。
public int offsetByCodePoints(int index, int codePointOffset) {
if (index < 0 || index > length()) {
throw new IndexOutOfBoundsException();
}
return Character.offsetByCodePoints(this, index, codePointOffset);
}
public static int offsetByCodePoints(CharSequence seq, int index,
int codePointOffset) {
int length = seq.length();
if (index < 0 || index > length) {
throw new IndexOutOfBoundsException();
}
int x = index;
if (codePointOffset >= 0) {
int i;
for (i = 0; x < length && i < codePointOffset; i++) {
if (isHighSurrogate(seq.charAt(x++)) && x < length &&
isLowSurrogate(seq.charAt(x))) {
x++;
}
}
if (i < codePointOffset) {
throw new IndexOutOfBoundsException();
}
} else {
int i;
for (i = codePointOffset; x > 0 && i < 0; i++) {
if (isLowSurrogate(seq.charAt(--x)) && x > 0 &&
isHighSurrogate(seq.charAt(x-1))) {
x--;
}
}
if (i < 0) {
throw new IndexOutOfBoundsException();
}
}
return x;
}
复制代码
getChars方法
用于获取字符串对象指定范围内的字符到目标 char 数组中,主要是根据两种编码做不同处理,如果是 LATIN1 编码则直接将 byte 数组对应索引的元素与0xff
做&操作并转成 char 类型。而如果是 UTF16 编码则需要两个字节一起转为 char 类型。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
checkBoundsBeginEnd(srcBegin, srcEnd, length());
checkBoundsOffCount(dstBegin, srcEnd - srcBegin, dst.length);
if (isLatin1()) {
StringLatin1.getChars(value, srcBegin, srcEnd, dst, dstBegin);
} else {
StringUTF16.getChars(value, srcBegin, srcEnd, dst, dstBegin);
}
}
复制代码
getBytes方法
获取字符串指定编码的字节数组,比如 charsetName 为 utf8,则将字符串转为 utf8 编码后对应的字节数组。如果不传参数则使用 JVM 默认编码,即Charset.defaultCharset()
。
public byte[] getBytes(String charsetName)
throws UnsupportedEncodingException {
if (charsetName == null) throw new NullPointerException();
return StringCoding.encode(charsetName, coder(), value);
}
public byte[] getBytes() {
return StringCoding.encode(coder(), value);
}
复制代码
equals方法
用于比较两字符串对象是否相等,如果引用相同则返回 true。否则判断比较对象是否为 String 类的实例,是的话转成 String 类型,接着比较编码是否相同,分别以 LATIN1 编码和 UTF16 编码进行比较。
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
复制代码
contentEquals方法
该方法用于比较字符串之间内容是否相等,逻辑为:
- 是否由 AbstractStringBuilder 实例化出来,是的话表示它为 StringBuilder 或 StringBuffer 对象。
- 如果为 StringBuffer 对象,说明它是线程安全的,需要做同步处理,调用
nonSyncContentEquals
方法。 - 如果为 StringBuilder 对象,它是非线程安全的,直接调用
nonSyncContentEquals
方法。 - 如果为 String 对象,则调用
equals
方法比较。 - 接下去的逻辑属于 CharSequence 对象时的逻辑。如果长度不相等直接返回 false。
- 如果为 Latin1 编码,则只比较一个字节。
- 如果为 UTF16 编码,则要两个字节地比较。
public boolean contentEquals(CharSequence cs) {
if (cs instanceof AbstractStringBuilder) {
if (cs instanceof StringBuffer) {
synchronized(cs) {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
} else {
return nonSyncContentEquals((AbstractStringBuilder)cs);
}
}
if (cs instanceof String) {
return equals(cs);
}
int n = cs.length();
if (n != length()) {
return false;
}
byte[] val = this.value;
if (isLatin1()) {
for (int i = 0; i < n; i++) {
if ((val[i] & 0xff) != cs.charAt(i)) {
return false;
}
}
} else {
if (!StringUTF16.contentEquals(val, cs, n)) {
return false;
}
}
return true;
}
复制代码
nonSyncContentEquals
方法逻辑为:
- 判断两个长度不相等则返回 false。
- 如果两者的编码相同,则一个个字节比较。
- 两者编码不同时,如果本身编码为 Latin1 编码,那就直接返回 false,而如果本身为 UTF16 编码,则两个字节地比较。
private boolean nonSyncContentEquals(AbstractStringBuilder sb) {
int len = length();
if (len != sb.length()) {
return false;
}
byte v1[] = value;
byte v2[] = sb.getValue();
if (coder() == sb.getCoder()) {
int n = v1.length;
for (int i = 0; i < n; i++) {
if (v1[i] != v2[i]) {
return false;
}
}
} else {
if (!isLatin1()) {
return false;
}
return StringUTF16.contentEquals(v1, v2, len);
}
return true;
}
复制代码
equalsIgnoreCase方法
该方法用于对比字符串是否相等,而且是忽略大小写。如果是自己与自己对比则不为空则为true,否则需要两者长度相等且regionMatches
方法返回true才为true。
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.length() == length())
&& regionMatches(true, 0, anotherString, 0, length());
}
复制代码
regionMatches
方法逻辑为:
- ignoreCase 如果为 false,即不忽略大小写,则需要调另外一个
regionMatches
方法,这里为 true,忽略此方法。 - 校验 offset 不能小于0且不能大于待比较长度。
-
coder() == other.coder()
为 true,即两者编码一样时,如果为 Latin1 编码,则以 Latin1 方式比较,否则以 UTF16 方式比较。 - 如果两者编码不同,则以 Latin1 编码与 UTF16 编码比较,主要就是将 Latin1 转为 UTF16。而如果是 UTF16 编码的话则将另外一个的 Latin1 转为 UTF16 后比较。
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
if (!ignoreCase) {
return regionMatches(toffset, other, ooffset, len);
}
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)length() - len)
|| (ooffset > (long)other.length() - len)) {
return false;
}
byte tv[] = value;
byte ov[] = other.value;
if (coder() == other.coder()) {
return isLatin1()
? StringLatin1.regionMatchesCI(tv, toffset, ov, ooffset, len)
: StringUTF16.regionMatchesCI(tv, toffset, ov, ooffset, len);
}
return isLatin1()
? StringLatin1.regionMatchesCI_UTF16(tv, toffset, ov, ooffset, len)
: StringUTF16.regionMatchesCI_Latin1(tv, toffset, ov, ooffset, len);
}
复制代码
compareTo方法
该方法用于比较两个字符串,主要的逻辑为:
-
coder() == anotherString.coder()
,即两者编码相同时,如果为 Latin1 编码则以 Latin1 的方式进行比较。否则以 UTF16 方式进行比较,具体如何比较下面以 Latin1 编码为例子。 - 两者编码不同时,则将 Latin1 编码转成 UTF16 后进行比较。
public int compareTo(String anotherString) {
byte v1[] = value;
byte v2[] = anotherString.value;
if (coder() == anotherString.coder()) {
return isLatin1() ? StringLatin1.compareTo(v1, v2)
: StringUTF16.compareTo(v1, v2);
}
return isLatin1() ? StringLatin1.compareToUTF16(v1, v2)
: StringUTF16.compareToLatin1(v1, v2);
}
复制代码
Latin1 编码的比较逻辑为:
- 首先分别获取两者的长度。
- 选取两者最小的长度。
- 开始遍历寻找两者不相同的字符,分别获取对应的 char 值并相减,大于0则说明第一个字符串大。
- 如果遍历完都相等,那么就看谁的长度长,第一个字符串长度较长就返回大于0。
public static int compareTo(byte[] value, byte[] other) {
int len1 = value.length;
int len2 = other.length;
int lim = Math.min(len1, len2);
for (int k = 0; k < lim; k++) {
if (value[k] != other[k]) {
return getChar(value, k) - getChar(other, k);
}
}
return len1 - len2;
}
复制代码
compareToIgnoreCase方法
该方法类似 compareTo 方法,只是忽略大小写。实现通过CaseInsensitiveComparator
内部类来实现。
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
复制代码
regionMatches方法
该方法用于检测指定区域字符串是否相等,其逻辑为:
- ignoreCase 如果为 false,即不忽略大小写,则需要调另外一个
regionMatches
方法。 - 校验 offset 不能小于0且不能大于待比较长度。
-
coder() == other.coder()
为 true,即两者编码一样时,如果为 Latin1 编码,则以 Latin1 方式比较,否则以 UTF16 方式比较。 - 如果两者编码不同,则以 Latin1 编码与 UTF16 编码比较,主要就是将 Latin1 转为 UTF16。而如果是 UTF16 编码的话则将另外一个的 Latin1 转为 UTF16 后比较。
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
if (!ignoreCase) {
return regionMatches(toffset, other, ooffset, len);
}
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)length() - len)
|| (ooffset > (long)other.length() - len)) {
return false;
}
byte tv[] = value;
byte ov[] = other.value;
if (coder() == other.coder()) {
return isLatin1()
? StringLatin1.regionMatchesCI(tv, toffset, ov, ooffset, len)
: StringUTF16.regionMatchesCI(tv, toffset, ov, ooffset, len);
}
return isLatin1()
? StringLatin1.regionMatchesCI_UTF16(tv, toffset, ov, ooffset, len)
: StringUTF16.regionMatchesCI_Latin1(tv, toffset, ov, ooffset, len);
}
复制代码
大小写敏感的比较逻辑:
- 检验两者的偏移的合法性,不能小于0,也不能超出特定长度。
- 如果
coder() == other.coder()
,即两者编码相同时,如果为 Latin1 编码则直接比较每个字节,而如果为 UTF16 编码则需要将位移和长度都扩大一倍,因为 UTF16 占用的空间是 Latin1 的两倍,然后再比较每个字节是否相等。 - 如果两者编码不相同时,不管两者谁是 Latin1 还是 UTF16 编码,都将它们转换成 char 类型再比较。
public boolean regionMatches(int toffset, String other, int ooffset, int len) {
byte tv[] = value;
byte ov[] = other.value;
if ((ooffset < 0) || (toffset < 0) ||
(toffset > (long)length() - len) ||
(ooffset > (long)other.length() - len)) {
return false;
}
if (coder() == other.coder()) {
if (!isLatin1() && (len > 0)) {
toffset = toffset << 1;
ooffset = ooffset << 1;
len = len << 1;
}
while (len-- > 0) {
if (tv[toffset++] != ov[ooffset++]) {
return false;
}
}
} else {
if (coder() == LATIN1) {
while (len-- > 0) {
if (StringLatin1.getChar(tv, toffset++) !=
StringUTF16.getChar(ov, ooffset++)) {
return false;
}
}
} else {
while (len-- > 0) {
if (StringUTF16.getChar(tv, toffset++) !=
StringLatin1.getChar(ov, ooffset++)) {
return false;
}
}
}
}
return true;
}
复制代码
startsWith方法
该方法用于检测字符串是否以某个前缀开始,并且可以指定偏移。逻辑为:
- 检查偏移和长度的合法性。
- 如果
coder() == prefix.coder()
,即两者编码相同时,如果为 Latin1 编码则直接比较每个字节是否相等,如果为 UTF16 编码则要将位移扩大一倍,再比较每个字节。 - 如果两者编码不相同,如果字符串为 Latin1 编码而前缀字符串为 UTF16 编码则无法比较直接返回 false。如果字符串为 UTF16 而前缀字符串为 Latin1,则转为 UTF16 进行比较。
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
public boolean startsWith(String prefix, int toffset) {
if (toffset < 0 || toffset > length() - prefix.length()) {
return false;
}
byte ta[] = value;
byte pa[] = prefix.value;
int po = 0;
int pc = pa.length;
if (coder() == prefix.coder()) {
int to = isLatin1() ? toffset : toffset << 1;
while (po < pc) {
if (ta[to++] != pa[po++]) {
return false;
}
}
} else {
if (isLatin1()) {
return false;
}
while (po < pc) {
if (StringUTF16.getChar(ta, toffset++) != (pa[po++] & 0xff)) {
return false;
}
}
}
return true;
}
复制代码
endsWith方法
该方法用于检查是否以某个字符串结尾,间接调用startsWith
方法即可实现。
public boolean endsWith(String suffix) {
return startsWith(suffix, length() - suffix.length());
}
复制代码
hashCode方法
该方法返回字符串对象的哈希值,如果已经有缓存了则直接返回,否则根据不同编码分别计算哈希值。
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
hash = h = isLatin1() ? StringLatin1.hashCode(value)
: StringUTF16.hashCode(value);
}
return h;
}
复制代码
下面分别是 Latin1 编码和 UTF16 编码的哈希值计算逻辑,遍历地执行h = 31 * h + (v & 0xff)
和 h = 31 * h + getChar(value, i)
运算。
public static int hashCode(byte[] value) {
int h = 0;
for (byte v : value) {
h = 31 * h + (v & 0xff);
}
return h;
}
public static int hashCode(byte[] value) {
int h = 0;
int length = value.length >> 1;
for (int i = 0; i < length; i++) {
h = 31 * h + getChar(value, i);
}
return h;
}
复制代码
indexOf方法
该方法用于查找字符串中第一个出现某字符或字符串的位置,有多种方法参数。可传入 int 类型,也可传入 String 类型,另外还能传入开始位置。根据编码的不同分别调用 StringLatin1 和 StringUTF16 的indexOf
方法。
public int indexOf(int ch) {
return indexOf(ch, 0);
}
public int indexOf(int ch, int fromIndex) {
return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex)
: StringUTF16.indexOf(value, ch, fromIndex);
}
public int indexOf(String str) {
if (coder() == str.coder()) {
return isLatin1() ? StringLatin1.indexOf(value, str.value)
: StringUTF16.indexOf(value, str.value);
}
if (coder() == LATIN1) {
return -1;
}
return StringUTF16.indexOfLatin1(value, str.value);
}
public int indexOf(String str, int fromIndex) {
return indexOf(value, coder(), length(), str, fromIndex);
}
复制代码
Latin1 编码查找逻辑,
- 判断 int 值是否能转成 byte,方法是看右移8位是否为0,为0即说明除了低8位其他都为0。
- 判断索引值的合法性并修正。
- int 值转成 byte 类型。
- 遍历检查数组中哪个值相等并返回对应索引值。
- 查找不到就返回-1。
public static int indexOf(byte[] value, int ch, int fromIndex) {
if (!canEncode(ch)) {
return -1;
}
int max = value.length;
if (fromIndex < 0) {
fromIndex = 0;
} else if (fromIndex >= max) {
return -1;
}
byte c = (byte)ch;
for (int i = fromIndex; i < max; i++) {
if (value[i] == c) {
return i;
}
}
return -1;
}
public static boolean canEncode(int cp) {
return cp >>> 8 == 0;
}
复制代码
类似地,对于 UTF16 编码也做类似处理,但因为 unicode 包含了基本多语言平面(Basic Multilingual Plane,BMP)外,还存在补充平面。而传入的值为 int 类型(4字节),所以如果超出 BMP 平面,此时需要4个字节,分别用来保存 High-surrogate 和 Low-surrogate,此时就需要对比4个字节。
另外,如果查找子字符串则是从子字符串第一个字符开始匹配直到子字符串完全被匹配成功。