string-source.md 21.0 KB
Newer Older
沉默王二's avatar
沉默王二 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
---
title: 深入解读String类源码及其应用技巧
shortTitle: 解读String类源码
category:
  - Java核心
tag:
  - 数组&字符串
description: 本文将对Java String类的源码进行深入分析,帮助你理解字符串在Java中的实现原理、操作方式和性能优化策略。通过详细解读String源码,我们将揭示字符串类的内部机制,掌握如何高效地进行字符串操作,以及在实际编程中如何应用这些知识来提高代码质量和性能。
head:
  - - meta
    - name: keywords
      content: Java, String类, 源码, 源码解析, 字符串操作,String
---

# 4.4 解读String类源码

我正坐在沙发上津津有味地读刘欣大佬的《码农翻身》——Java 帝国这一章,门铃响了。起身打开门一看,是三妹,她从学校回来了。

“三妹,你回来的真及时,今天我们打算讲 Java 中的字符串呢。”等三妹换鞋的时候我说。

沉默王二's avatar
沉默王二 已提交
21
“哦,可以呀,哥。听说字符串的细节特别多,什么[字符串常量池](https://javabetter.cn/string/constant-pool.html)了、字符串不可变性了、[字符串拼接](https://javabetter.cn/string/join.html)了、字符串长度限制了等等,你最好慢慢讲,否则我可能一时半会消化不了。”三妹的态度显得很诚恳。
沉默王二's avatar
沉默王二 已提交
22 23 24 25 26

“嗯,我已经想好了,今天就只带你大概认识一下字符串,主要读一读它的源码,其他的细节咱们后面再慢慢讲,保证你能及时消化。”

“好,那就开始吧。”三妹已经准备好坐在了电脑桌的边上。

沉默王二's avatar
沉默王二 已提交
27
我应了一声后走到电脑桌前坐下来,顺手打开 [Intellij IDEA](https://javabetter.cn/overview/IDEA-install-config.html),并找到了 String 的源码(Java 8)。
沉默王二's avatar
沉默王二 已提交
28 29 30 31 32 33 34 35 36

### String 类的声明

```java
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
}
```

沉默王二's avatar
沉默王二 已提交
37
“第一,String 类是 [final](https://javabetter.cn/oo/final.html) 的,意味着它不能被子类继承。”
沉默王二's avatar
沉默王二 已提交
38

沉默王二's avatar
沉默王二 已提交
39
“第二,String 类实现了 [Serializable 接口](https://javabetter.cn/io/Serializbale.html),意味着它可以[序列化](https://javabetter.cn/io/serialize.html)。”
沉默王二's avatar
沉默王二 已提交
40

沉默王二's avatar
沉默王二 已提交
41
“第三,String 类实现了 [Comparable 接口](https://javabetter.cn/basic-extra-meal/comparable-omparator.html),意味着最好不要用‘==’来[比较两个字符串是否相等](https://javabetter.cn/string/equals.html),而应该用 `compareTo()` 方法去比较。”
沉默王二's avatar
沉默王二 已提交
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68

因为 == 是用来比较两个对象的地址,这个在讲字符串比较的时候会详细讲。如果只是说比较字符串内容的话,可以使用 String 类的 equals 方法:

```java
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true;
    }
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}
```

沉默王二's avatar
沉默王二 已提交
69
“第四,[StringBuffer、StringBuilder 和 String](https://javabetter.cn/string/builder-buffer.html) 一样,都实现了 CharSequence 接口,所以它们仨属于近亲。由于 String 是不可变的,所以遇到字符串拼接的时候就可以考虑一下 String 的另外两个好兄弟,StringBuffer 和 StringBuilder,它俩是可变的。”
沉默王二's avatar
沉默王二 已提交
70 71 72 73 74 75 76

### String 类的底层实现

```java
private final char value[];
```

沉默王二's avatar
沉默王二 已提交
77
“第五,Java 9 以前,String 是用 char 型[数组](https://javabetter.cn/array/array.html)实现的,之后改成了 byte 型数组实现,并增加了 coder 来表示编码。这样做的好处是在 Latin1 字符为主的程序里,可以把 String 占用的内存减少一半。当然,天下没有免费的午餐,这个改进在节省内存的同时引入了编码检测的开销。”
沉默王二's avatar
沉默王二 已提交
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108

>Latin1(Latin-1)是一种单字节字符集(即每个字符只使用一个字节的编码方式),也称为ISO-8859-1(国际标准化组织8859-1),它包含了西欧语言中使用的所有字符,包括英语、法语、德语、西班牙语、葡萄牙语、意大利语等等。在Latin1编码中,每个字符使用一个8位(即一个字节)的编码,可以表示256种不同的字符,其中包括ASCII字符集中的所有字符,即0x00到0x7F,以及其他西欧语言中的特殊字符,例如é、ü、ñ等等。由于Latin1只使用一个字节表示一个字符,因此在存储和传输文本时具有较小的存储空间和较快的速度

```java
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    @Stable
    private final byte[] value;
    private final byte coder;
    private int hash;
}
```

接下来,我们来详细地说一下。

`char[]``byte[]`,最主要的目的是**节省字符串占用的内存空间**。内存占用减少带来的另外一个好处,就是 GC 次数也会减少。

我们使用 `jmap -histo:live pid | head -n 10` 命令就可以查看到堆内对象示例的统计信息、查看 ClassLoader 的信息以及 finalizer 队列。

以我正在运行着的[编程喵](https://github.com/itwanger/coding-more)项目实例(基于 Java 8)来说,结果是这样的。

![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/basic-extra-meal/jdk9-char-byte-string-d826ce88-bbbe-47a3-a1a9-4dd86dd3632f.png)

其中 String 对象有 17638 个,占用了 423312 个字节的内存,排在第三位。

由于 Java 8 的 String 内部实现仍然是 `char[]`,所以我们可以看到内存占用排在第 1 位的就是 char 数组。

`char[]` 对象有 17673 个,占用了 1621352 个字节的内存,排在第一位。

那也就是说优化 String 节省内存空间是非常有必要的,如果是去优化一个使用频率没有 String 这么高的类,就没什么必要,对吧?

沉默王二's avatar
沉默王二 已提交
109
众所周知,char 类型的数据在 JVM 中是占用两个字节的,并且使用的是 UTF-8 [编码](https://javabetter.cn/basic-extra-meal/java-unicode.html),其值范围在 '\u0000'(0)和 '\uffff'(65,535)(包含)之间。
沉默王二's avatar
沉默王二 已提交
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182

也就是说,使用 `char[]` 来表示 String 就会导致,即使 String 中的字符只用一个字节就能表示,也得占用两个字节。

>PS:在计算机中,单字节字符通常指的是一个字节(8位)可以表示的字符,而双字节字符则指需要两个字节(16位)才能表示的字符。单字节字符和双字节字符的定义是相对的,不同的编码方式对应的单字节和双字节字符集也不同。常见的单字节字符集有ASCII(美国信息交换标准代码)、ISO-8859(国际标准化组织标准编号8859)、GBK(汉字内码扩展规范)、GB2312(中国国家标准,现在已经被GBK取代),像拉丁字母、数字、标点符号、控制字符都是单字节字符。双字节字符集包括 Unicode、UTF-8、GB18030(中国国家标准),中文、日文、韩文、拉丁文扩展字符属于双字节字符。

当然了,仅仅将 `char[]` 优化为 `byte[]` 是不够的,还要配合 Latin-1 的编码方式,该编码方式是用单个字节来表示字符的,这样就比 UTF-8 编码节省了更多的空间。

换句话说,对于:

```java
String name = "jack";
```

这样的,使用 Latin-1 编码,占用 4 个字节就够了。

但对于:

```java
String name = "小二";
```

这种,木的办法,只能使用 UTF16 来编码。

针对 JDK 9 的 String 源码里,为了区别编码方式,追加了一个 coder 字段来区分。

```java
/**
 * The identifier of the encoding used to encode the bytes in
 * {@code value}. The supported values in this implementation are
 *
 * LATIN1
 * UTF16
 *
 * @implNote This field is trusted by the VM, and is a subject to
 * constant folding if String instance is constant. Overwriting this
 * field after construction will cause problems.
 */
private final byte coder;
```

Java 会根据字符串的内容自动设置为相应的编码,要么 Latin-1 要么 UTF16。

也就是说,从 `char[]``byte[]`**中文是两个字节,纯英文是一个字节,在此之前呢,中文是两个字节,英文也是两个字节**

在 UTF-8 中,0-127 号的字符用 1 个字节来表示,使用和 ASCII 相同的编码。只有 128 号及以上的字符才用 2 个、3 个或者 4 个字节来表示。

- 如果只有一个字节,那么最高的比特位为 0;
- 如果有多个字节,那么第一个字节从最高位开始,连续有几个比特位的值为 1,就使用几个字节编码,剩下的字节均以 10 开头。

具体的表现形式为:

- 0xxxxxxx:一个字节;
- 110xxxxx 10xxxxxx:两个字节编码形式(开始两个 1);
- 1110xxxx 10xxxxxx 10xxxxxx:三字节编码形式(开始三个 1);
- 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx:四字节编码形式(开始四个 1)。

也就是说,UTF-8 是变长的,那对于 String 这种有随机访问方法的类来说,就很不方便。所谓的随机访问,就是charAt、subString这种方法,随便指定一个数字,String要能给出结果。如果字符串中的每个字符占用的内存是不定长的,那么进行随机访问的时候,就需要从头开始数每个字符的长度,才能找到你想要的字符。

那你可能会问,UTF-16也是变长的呢?一个字符还可能占用 4 个字节呢?

的确,UTF-16 使用 2 个或者 4 个字节来存储字符。

- 对于 Unicode 编号范围在 0 ~ FFFF 之间的字符,UTF-16 使用两个字节存储。
- 对于 Unicode 编号范围在 10000 ~ 10FFFF 之间的字符,UTF-16 使用四个字节存储,具体来说就是:将字符编号的所有比特位分成两部分,较高的一些比特位用一个值介于 D800~DBFF 之间的双字节存储,较低的一些比特位(剩下的比特位)用一个值介于 DC00~DFFF 之间的双字节存储。

但是在 Java 中,一个字符(char)就是 2 个字节,占 4 个字节的字符,在 Java 里也是用两个 char 来存储的,而String的各种操作,都是以Java的字符(char)为单位的,charAt是取得第几个char,subString取的也是第几个到第几个char组成的子串,甚至length返回的都是char的个数。

所以UTF-16在Java的世界里,就可以视为一个定长的编码。

>参考链接:[https://www.zhihu.com/question/447224628](https://www.zhihu.com/question/447224628)

### String 类的 hashCode 方法

沉默王二's avatar
沉默王二 已提交
183
“第六,每一个字符串都会有一个 hash 值,这个哈希值在很大概率是不会重复的,因此 String 很适合来作为 [HashMap](https://javabetter.cn/collection/hashmap.html) 的键值。”
沉默王二's avatar
沉默王二 已提交
184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215

来看 String 类的 hashCode 方法。

```java
private int hash; // Cache the hash code for the string

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}
```

hashCode 方法首先检查是否已经计算过哈希码,如果已经计算过,则直接返回缓存的哈希码。否则,方法将使用一个循环遍历字符串的所有字符,并使用一个乘法和加法的组合计算哈希码。这种计算方法被称为“31 倍哈希法”。计算完成后,将得到的哈希值存储在 hash 成员变量中,以便下次调用 hashCode 方法时直接返回该值,而不需要重新计算。这是一种缓存优化,称为“惰性计算”。

31倍哈希法(31-Hash)是一种简单有效的字符串哈希算法,常用于对字符串进行哈希处理。该算法的基本思想是将字符串中的每个字符乘以一个固定的质数31的幂次方,并将它们相加得到哈希值。具体地,假设字符串为s,长度为n,则31倍哈希值计算公式如下:

```
H(s) = (s[0] * 31^(n-1)) + (s[1] * 31^(n-2)) + ... + (s[n-1] * 31^0)
```

其中,s[i]表示字符串s中第i个字符的ASCII码值,`^`表示幂运算。

31倍哈希法的优点在于简单易实现,计算速度快,同时也比较均匀地分布在哈希表中。

沉默王二's avatar
沉默王二 已提交
216
[hashCode 方法](https://javabetter.cn/basic-extra-meal/hashcode.html),我们会在另外一个章节里详细讲,戳前面的链接了解。
沉默王二's avatar
沉默王二 已提交
217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265

我们可以通过以下方法模拟 String 的 hashCode 方法:

```java
public class HashCodeExample {
    public static void main(String[] args) {
        String text = "沉默王二";
        int hashCode = computeHashCode(text);
        System.out.println("字符串 \"" + text + "\" 的哈希码是: " + hashCode);

        System.out.println("String 的 hashCode " + text.hashCode());
    }

    public static int computeHashCode(String text) {
        int h = 0;
        for (int i = 0; i < text.length(); i++) {
            h = 31 * h + text.charAt(i);
        }
        return h;
    }
}
```

看一下结果:

```
字符串 "沉默王二" 的哈希码是: 867758096
String 的 hashCode 867758096
```

结果是一样的,又学到了吧?

### String 类的 substring 方法

String 类中还有一个方法比较常用 substring,用来截取字符串的,来看源码。

```java
public String substring(int beginIndex) {
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    int subLen = value.length - beginIndex;
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}
```

沉默王二's avatar
沉默王二 已提交
266
substring 方法首先检查参数的有效性,如果参数无效,则抛出 StringIndexOutOfBoundsException [异常](https://javabetter.cn/exception/gailan.html)。接下来,方法根据参数计算子字符串的长度。如果子字符串长度小于零,抛出StringIndexOutOfBoundsException异常。
沉默王二's avatar
沉默王二 已提交
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440

如果 beginIndex 为 0,且 endIndex 等于字符串的长度,说明子串与原字符串相同,因此直接返回原字符串。否则,使用 value 数组(原字符串的字符数组)的一部分创建一个新的 String 对象并返回。

下面是几个使用 substring 方法的示例:

①、提取字符串中的一段子串:

```java
String str = "Hello, world!";
String subStr = str.substring(7, 12);  // 从第7个字符(包括)提取到第12个字符(不包括)
System.out.println(subStr);  // 输出 "world"
```

②、提取字符串中的前缀或后缀:

```java
String str = "Hello, world!";
String prefix = str.substring(0, 5);  // 提取前5个字符,即 "Hello,"
String suffix = str.substring(7);     // 提取从第7个字符开始的所有字符,即 "world!"
```

③、处理字符串中的空格和分隔符:

```java
String str = "   Hello,   world!  ";
String trimmed = str.trim();                  // 去除字符串开头和结尾的空格
String[] words = trimmed.split("\\s+");       // 将字符串按照空格分隔成单词数组
String firstWord = words[0].substring(0, 1);  // 提取第一个单词的首字母
System.out.println(firstWord);                // 输出 "H"
```

④、处理字符串中的数字和符号:

```java
String str = "1234-5678-9012-3456";
String[] parts = str.split("-");             // 将字符串按照连字符分隔成四个部分
String last4Digits = parts[3].substring(1);  // 提取最后一个部分的后三位数字
System.out.println(last4Digits);             // 输出 "456"
```

总之,substring 方法可以根据需求灵活地提取字符串中的子串,为字符串处理提供了便利。

### String 类的 indexOf 方法

indexOf 方法用于查找一个子字符串在原字符串中第一次出现的位置,并返回该位置的索引。来看该方法的源码:

```java
/*
 * 查找字符数组 target 在字符数组 source 中第一次出现的位置。
 * sourceOffset 和 sourceCount 参数指定 source 数组中要搜索的范围,
 * targetOffset 和 targetCount 参数指定 target 数组中要搜索的范围,
 * fromIndex 参数指定开始搜索的位置。
 * 如果找到了 target 数组,则返回它在 source 数组中的位置索引(从0开始),
 * 否则返回-1。
 */
static int indexOf(char[] source, int sourceOffset, int sourceCount,
        char[] target, int targetOffset, int targetCount,
        int fromIndex) {
    // 如果开始搜索的位置已经超出 source 数组的范围,则直接返回-1(如果 target 数组为空,则返回 sourceCount)
    if (fromIndex >= sourceCount) {
        return (targetCount == 0 ? sourceCount : -1);
    }
    // 如果开始搜索的位置小于0,则从0开始搜索
    if (fromIndex < 0) {
        fromIndex = 0;
    }
    // 如果 target 数组为空,则直接返回开始搜索的位置
    if (targetCount == 0) {
        return fromIndex;
    }

    // 查找 target 数组的第一个字符在 source 数组中的位置
    char first = target[targetOffset];
    int max = sourceOffset + (sourceCount - targetCount);

    // 循环查找 target 数组在 source 数组中的位置
    for (int i = sourceOffset + fromIndex; i <= max; i++) {
        /* Look for first character. */
        // 如果 source 数组中当前位置的字符不是 target 数组的第一个字符,则在 source 数组中继续查找 target 数组的第一个字符
        if (source[i] != first) {
            while (++i <= max && source[i] != first);
        }

        /* Found first character, now look at the rest of v2 */
        // 如果在 source 数组中找到了 target 数组的第一个字符,则继续查找 target 数组的剩余部分是否匹配
        if (i <= max) {
            int j = i + 1;
            int end = j + targetCount - 1;
            for (int k = targetOffset + 1; j < end && source[j]
                    == target[k]; j++, k++);

            // 如果 target 数组全部匹配,则返回在 source 数组中的位置索引
            if (j == end) {
                /* Found whole string. */
                return i - sourceOffset;
            }
        }
    }
    // 没有找到 target 数组,则返回-1
    return -1;
}
```

来看示例。

①、示例1:查找子字符串的位置

```java
String str = "Hello, world!";
int index = str.indexOf("world");  // 查找 "world" 子字符串在 str 中第一次出现的位置
System.out.println(index);        // 输出 7
```

②、示例2:查找字符串中某个字符的位置

```java
String str = "Hello, world!";
int index = str.indexOf(",");     // 查找逗号在 str 中第一次出现的位置
System.out.println(index);        // 输出 5
```

③、示例3:查找子字符串的位置(从指定位置开始查找)

```java
String str = "Hello, world!";
int index = str.indexOf("l", 3);  // 从索引为3的位置开始查找 "l" 子字符串在 str 中第一次出现的位置
System.out.println(index);        // 输出 3
```

④、示例4:查找多个子字符串

```java
String str = "Hello, world!";
int index1 = str.indexOf("o");    // 查找 "o" 子字符串在 str 中第一次出现的位置
int index2 = str.indexOf("o", 5); // 从索引为5的位置开始查找 "o" 子字符串在 str 中第一次出现的位置
System.out.println(index1);       // 输出 4
System.out.println(index2);       // 输出 8
```

### String 类的其他方法

比如说 `length()` 用于返回字符串长度。

比如说 `isEmpty()` 用于判断字符串是否为空。

比如说 `charAt()` 用于返回指定索引处的字符。

比如说 `getBytes()` 用于返回字符串的字节数组,可以指定编码方式,比如说:

```java
String text = "沉默王二";
System.out.println(Arrays.toString(text.getBytes(StandardCharsets.UTF_8)));
```

比如说 `trim()` 用于去除字符串两侧的空白字符,来看源码:

```java
public String trim() {
    int len = value.length;
    int st = 0;
    char[] val = value;    /* avoid getfield opcode */

    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
```

举例:`"  沉默王二   ".trim()` 会返回"沉默王二"

沉默王二's avatar
沉默王二 已提交
441
除此之外,还有 [split](https://javabetter.cn/string/split.html)[equals](https://javabetter.cn/string/equals.html)[join](https://javabetter.cn/string/join.html) 等这些方法,我们后面会一一来细讲。
沉默王二's avatar
沉默王二 已提交
442 443 444

---

沉默王二's avatar
9000+  
沉默王二 已提交
445
GitHub 上标星 9000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 9000+ 的 Java 教程](https://javabetter.cn/overview/)
沉默王二's avatar
沉默王二 已提交
446

沉默王二's avatar
沉默王二 已提交
447 448

微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。
沉默王二's avatar
沉默王二 已提交
449 450

![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png)