参考资料:

String 为什么不可变?不可变有什么好处?

什么是不可变类?

不可变指的是一个类的对象一旦创建,其状态(属性和方法)就不可改变。
如以下代码中的 test 这个变量,它改变的只是引用,也就是说 test 这个引用原本指向的是 finalTest,赋值成 fianl 之后只是改变了指针指向的对象而已。

  1. public void testFinal(){
  2. String test = "fianlTest";
  3. // 等价于 String test = new String("finalTest");
  4. test = "final";
  5. // 等价于 test = new String("final");
  6. }

不可变类的设计原则

  1. 类添加 final 修饰,保证类不被继承

如果不在类声明中添加 final,那么我们就可以通过继承产生子类,子类重写父类方法改变其成员变量,最后父类引用指向子类对象(多态)的方式改变一个对象的状态。
这一点在 String 类中也有体现。
不可变类(以String为例) - 图1

  1. 保证成员变量必须私有,并且使用 final 修饰

通过2、3方法结合的形式阻止对象不可在类的外部直接访问成员变量,在类的内部也无法改变其引用,但是这种方法还有一个缺陷,就是可以通过构造器传入外部成员变量,这一点在第4步解决。
这一点在 String 类中也有体现。
不可变类(以String为例) - 图2

  1. 不提供改变成员变量的方法,包括 setter

避免通过其他接口改变成员变量的值,破坏不可变特性。
这一点在String 类中也有体现,仔细查看可以发现,String 中所有看似更改了值的方法实际上最后都是返回了一个新的对象。
不可变类(以String为例) - 图3

  1. 通过构造器初始化成员变量时,使用深拷贝(deep copy)
    这是为了防止构造器传入外部引用,再通过修改这个外部引用的方式改变类的状态,例如: ```java public class Demo{ public static test() {
    1. char[] value = "hello world".toCharArray();
    2. Test t = new Test(value);
    3. value[5] = '_';
    } }

class Test{ private final char[] value; public Test(char[] value) { this.value = value; } }

class Test01{ private final char[] value; public Test(char[] value) { this.value = value.clone(); } }

在上面这个例子中,Test 类存在将外部变量传入构造器,再通过修改外部变量改变 Test 的状态的隐患,而 Test01 则无法通过这种方式修改对象的状态。<br />这一点在String 类中也有体现。<br />![](https://cdn.nlark.com/yuque/0/2022/png/22363114/1656658043109-33980d9c-df0d-49d2-affe-4904d3274ad4.png#crop=0&crop=0&crop=1&crop=1&from=url&id=LqWOZ&margin=%5Bobject%20Object%5D&originHeight=318&originWidth=1300&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)

5. **在 getter 中,不要直接返回对象的成员变量,而是返回成员变量的拷贝**<br />跟第4点同理,这一点在String 类中也有体现

![](https://cdn.nlark.com/yuque/0/2022/png/22363114/1656658103818-45a23583-d571-4580-b2b1-1adc1f9d0505.png#crop=0&crop=0&crop=1&crop=1&from=url&id=xeBub&margin=%5Bobject%20Object%5D&originHeight=237&originWidth=1239&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=)
<a name="cEdgp"></a>
## 使用不可变类的好处
<a name="BS1Cb"></a>
###  安全性
防止String 在传递过程中发生改变,导致非法参数无法被拦截<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22363114/1656658389847-d6307a5c-7629-48d9-aca5-43bf83228ebf.png#clientId=u55d8c9a1-e48b-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=251&id=ue06e4daf&margin=%5Bobject%20Object%5D&name=image.png&originHeight=501&originWidth=1247&originalType=binary&ratio=1&rotation=0&showTitle=false&size=181429&status=done&style=none&taskId=ubcd4a875-3602-496a-8a59-6506c3296c1&title=&width=623.5)<br />以上代码中,如果String 对象可变,那么 myString 对象可能会在传递的过程中被改变,导致一些不可预知的事件发生。
<a name="ZFUFL"></a>
### 节省空间——字符串常量池
总所周知,String 对象的引用是存储在字符串常量池当中的,相同的 String 对象不需要创建重复的对象,而是指向同一个引用,这一点有利有弊,优点在于节省空间,缺点则在于一个对象的改变会导致所有指向这个对象的引用全部改变。
<a name="cdwGv"></a>
### 线程安全
同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。
<a name="mvipD"></a>
### 性能考虑
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串的原因。
<a name="QofBJ"></a>
## 不可变类真的不可改变吗?
先说结论:并不是,可以通过反射直接修改 String 的值
```java
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World"; 
System.out.println("s = " + s); //Hello World

//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);

//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s);  //Hello_World

输出结果为:

s = Hello World
s = Hello_World