---
String
---

#### 目录

1. 前言
2. 思维导图
3. 源码解析
   - 类继承关系
   - 类成员变量
   - 类成员方法
   - 相关静态方法
4. StringBuilder 和 StringBuffer
5. 对象内存分配
6. 常见面试题
7. 参考

#### 前言

对于 String，大家可能再常见不过了。我们知道，String 是一个不可变类，也由于它的不可变性，所以在类似拼接、裁剪字符串时，都会产生新的 String 对象。字符串操作不当就可能产生大量临时字符串，这也就引入了 StringBuilder 和 StringBuffer，它们之间的区别就在于是否线程安全。当然，你可能也知道 new String("Omooo") 和 String name = "Omooo" 的区别。

那么你是否知道 JDK9 之后对 String 实现的变化呢？StringBuilder 和 StringBuffer 除了线程安全还有哪些知识点呢？以及对 String 的编译器优化、+ 重载符的底层实现、intern() 方法的版本区别？

#### 思维导图

![](https://i.loli.net/2019/03/01/5c7941e96bcb1.png)

#### 源码解析

##### 类继承关系

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

String 类由 final 修饰，是一个不可变类。

##### 类成员变量

```java
// JDK 10
private final byte[] value;
private final byte coder;	//采取的编码方式
static final boolean COMPACT_STRINGS;

static {
    COMPACT_STRINGS = true;		//压缩 String 存储空间
}
@Native static final byte LATIN1 = 0;
@Native static final byte UTF16  = 1;
```

在 JDK 9 之前，用的是 char 数组来存储 String 的值，之后就用 byte 数组来存储，一个 char 是两个 byte，在存储单个字符时以及拉丁语系的字符时，根本就不需要太宽的 char，所以在之后用了划分粒度更细的 byte 来存储，这也就带来了更小的内存占用和更快的操作速度。

String 支持多种编码，但是如果不指定编码的话，它可能使用两种编码方式，分别是 LATIN1 和 UTF16，LATIN1 其实就是 ISO 编码，属于单字节编码，而 UTF16 为双字节编码。

String 在表示因为字符或者数字时，会可能存在浪费空间的情况，比如在存储 what 字符串时：

![](https://i.loli.net/2018/12/31/5c29a0e29ddef.png)

在 java 9 之后就变成了：

![](https://i.loli.net/2018/12/31/5c29a1094834b.png)

可以看到，压缩之后存储更加紧凑了。默认是开启压缩的，即 COMPACT_STRINGS 默认为 true。

##### 类成员方法

```java
   //计算长度
	public int length() {
        return value.length >> coder();
    }
    byte coder() {
        return COMPACT_STRINGS ? coder : UTF16;
    }
	//获取指定位置的字符
    public char charAt(int index) {
        if (isLatin1()) {
            return StringLatin1.charAt(value, index);
        } else {
            return StringUTF16.charAt(value, index);
        }
    }
	//...
```

既然改变了编码方式，计算长度就需要考虑编码方式了，如果是 UTF16，双字节编码，那就是右移一位即长度为之前的 1/2。同时，也能看出来，默认采用的是单字节编码即 ISO 编码（COMPACT_STRINGS 默认为 true）。

剩下就是 String#intern() 方法：

```java
public native String intern();
```

intern() 是在 Java6 引入的，目的是提示 JVM 把相应字符串放在常量池缓存起来。在我们创建字符串对象并调用 intern() 方法的时候，如果已经有了缓存的字符串，就会返回缓存里面的实例，否则就将其缓存起来。

在使用 Java6 中并不推荐大量使用 intern() 方法，这是因为被缓存的字符串是存在所谓的 PermGen 里的，也就是永久代，这个空间是有限的，也基本不会被除了 FullGC 之外的垃圾收集照顾到，所以，如果使用不当，OOM 就会光顾。在后续版本中，这个缓存被放置在堆上，这样就极大避免了永久代被占满的问题，甚至永久代在 JDK8 中被 MetaSpace（元数据区）替代了。而且，默认缓存大小也在不断的扩大中。

还有就是在 Java7 中，intern 方法做了些改变，进行拷贝的时候不是拷贝对象，而是拷贝地址值。这里强烈推荐 [String类相关面试题很难？不要方，问题不大](https://www.jianshu.com/p/d416a074409d) 这一篇文章。

Intern 是一种显式的重排机制，但是它也有一定的副作用，那就是需要手动调用。我想基本上很少有人用到这个方法，因为我们很难预计字符串的重复情况，反而是一种看视一种冗余的操作。好在，在 JDK8 中，推出了一种新特性，那就是 G1 GC 下的字符串重排，它是通过将相同数据的字符串指向同一份数据来做到的，是 JVM 底层的改变，并不需要 Java 类库做什么修改。

#### StringBuilder 和 StringBuffer

StringBuffer 是 StringBuilder 的线程安全版本，二者都继承了 AbstractStringBuilder，StringBuffer 在修改字符串操作
（append、replace、substring 等）的时候都加了 synchronized，这里就以 StringBuilder 来分析。

```java
StringBuilder sb = new StringBuilder("Om");
String name = sb.append("o").append("o").append("o").toString();
```

首先需要明确的就是 StringBuilder 和 StringBuffer 针对字符串的修改都是通过 byte[] 数组的。

既然是通过内部数组来实现的，那么内部数组应该创建多大呢？

```java
public StringBuilder(String str) {
   	//1.创建一个初始容量+16 的 byte[] 数组
	super(str.length() + 16);
    //2.把原 byte[] 数组的值拷贝到新数组
	append(str);
}

	//super(str.length() + 16)调用的 AbstractStringBuilder 的构造方法
    AbstractStringBuilder(int capacity) {
        if (COMPACT_STRINGS) {
            value = new byte[capacity];
            coder = LATIN1;
        } else {
            value = StringUTF16.newBytesFor(capacity);
            coder = UTF16;
        }
    }
```

所以，当如果已知可能拼接的字符串长度过时，可以这样指定：

```java
StringBuilder sb = new StringBuilder(30);
```

这就避免了拼接了长度大于十六之后导致数组拷贝的开销，在循环中拼接字符串要特别注意。

还有一种情况，那就是在 for 循环中使用 + 拼接字符串，这也是要极力避免的，因为 +  重载符底层也是通过 StringBuilder 来实现的，实际上这句话也不太准确，因为存在编译器优化的情况。

```java
public class StringTest {

    public static void main(String[] args) {
        String s = "Name: " + "Omooo";
        System.out.println(s);
        System.out.println(add("Omooo"));
    }

    private static String add(String name) {
        return "Name: " + name;
    }
}
```

由于字符串的不可变性，所以在编译阶段就能确定 s 的值，所以也就不需要 StringBuilder。对于 add 方法，我们直接看编译后的字节码即可（IDEA -> View -> Show Bytecode）：

```java
   L0
    LINENUMBER 11 L0
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "Name: "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE name Ljava/lang/String; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 1
```

可以明显的看出是通过 StringBuilder 来实现的，以上是在 JDK8 环境，对于 JDK8 之后则是：

```java
  private static add(Ljava/lang/String;)Ljava/lang/String;
   L0
    LINENUMBER 12 L0
    ALOAD 0
    INVOKEDYNAMIC makeConcatWithConstants(Ljava/lang/String;)Ljava/lang/String; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/StringConcatFactory.makeConcatWithConstants(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;
      // arguments:
      "Name: \u0001"
    ]
    ARETURN
   L1
    LOCALVARIABLE name Ljava/lang/String; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
```

可以看到是通过 StringConcatFactory.makeConcatWithConstants ，查看源码其实内部也是通过 StringBuilder 来实现的。

#### 对象内存分配

关于内存分配这一块，再次推荐一遍 [String类相关面试题很难？不要方，问题不大](https://www.jianshu.com/p/d416a074409d) 这一篇文章。

String 对象创建有两种方式：

1. 字面量赋值

   ```java
   String str = "Omooo"
   ```

   这样创建字符串对象，首先会去常量池中找有没有这个字符串，如果有就直接指向，没有就先往常量池中添加再指向。

2. new 创建

   ```java
   String str = new String("Omooo");
   ```

   当然，我们肯定不会这样写。如果这样写了，它会做两件事。

   首先在堆上创建该字符串对象，然后去看常量池中是否有该字符串，如果有就算了，没有就往常量池中添加一个。

String 对象的内存分配讲完了，那就看这一道题：

```java
String str1 = new String("str")+new String("01");
str1.intern();
String str2 = "str01";
System.out.println(str2==str1);
```

输出 true。

在 JDK 1.7 之后，intern 方法做了些改变，进行拷贝的时候不是拷贝对象，而是拷贝地址值。

那么在想想一下两个呢？

```java
String str1 = new String("str")+new String("01");
String str2 = "str01";
str1.intern();
System.out.println(str2==str1);

String str1 = new String("str")+new String("01");
String str2 = "str01";
str1 = str1.intern();
System.out.println(str2==str1);
```

#### 常见面试题

1. String、StringBuilder、StingBuffer 的区别？
2. String name = "Omooo" 和 String name = new String("Omoo") 、String name = new String("Omoo") + "o" 的区别以及分别创建了多少个对象？

#### 参考

[String 源码浅析(一)](https://juejin.im/post/5c2588d8f265da6110371d2b)

[Java9后String的空间优化](https://blog.csdn.net/wangyangzhizhou/article/details/80371653)

[String类相关面试题很难？不要方，问题不大](https://www.jianshu.com/p/d416a074409d)

[G1垃圾回收器中的字符串去重](http://www.importnew.com/23354.html)

[第5讲 | String、StringBuffer、StringBuilder有什么区别？](https://time.geekbang.org/column/article/7349)