String

首先我们要有一个概念,互联网基本上就只干一件事「处理字符串」。我们看的纷繁复杂的网页,都是通过字节传输的,然后经过一个指定的编码转化成人类能看懂的字符串。因此能处理好字符串是 Web 服务器的基本要求,像 Java,PHP,Python,Ruby等。

String 的不可变性

背过一些八股文面试题的人都知道 String 是不可变的,那么问题来了,「不可变的定义是什么?」,「String 是如何保证不可变的呢?」

不可变性,当创建一个String s = "abcd"对象的时候,JVM 的堆内存就生成了一个 String 对象,并且它的值是 abcd,而 s 变量只是指向这个对象。我们说的不可变性指的就是 JVM 生成的对象值不可变,比如刚才的 abcd,创建出来之后它就一直都是 abcd,无法被改变。

经常使用字符串的人开始疑惑了,那我 String s = "abcdel"不是也可以吗?String 的值不是改变了吗?

的确,我们是可以给 s 这个变量重新赋值。但是我们给他赋值实际上是 JVM 新创建了一个值为 abcdel 的 String 变量,然后变量 s 从指向值为 abcd 的对象,改为指向值为 abcdel 的对象。原来值为 123 的对象还是存在 JVM 中,并且值并没有改变,只是暂时没有变量指向这个对象罢了。上面的操作,可以用如图所示。

image.png

了解上面的知识后,回到新的问题上来,String 是如何保证不可变性呢?

打开 String 的源代码,我们可以发现,String 类以及它的字符数组变量都是被 final 关键词修饰的。这就意味着 String 无法被继承,并且 char[] value 的地址指向无法修改,而且 String 所有的公开 API 都没有修改 char[] value 的方法,这就保证了 String 的不可变性。

image.png

优点

线程安全,不可变的对象天生就是线程安全的,可以随心所欲的在线程之间传递。

存储安全,为什么说是存储安全呢?因为遵守了 hashCode 的约定。

正是因为 Stirng 的不可变性,才能实现存储安全。我们可以想象一下有一个 Map<String,String> map = new HashMap<>();,假如 String 是可变的话,那么就违反了 hashCode 的约定,想了解 hashCode 的相关约定访问这里

如图所示,有三个字符串 “a”,”c”,通过计算分别得到了对应的 hashCode。如果 String 是可变的行不行呢?我们可以通过反证法来证明。现在假如 String 是可变的,那么当字符串从 a 变成了 c(即 String a = "a" 到 a = "c" ),他的 hashCode 要不要改变呢?分两种情况。

image.png

如果不改变 hashCode,那就违反了 hashCode  的第二条约定「两个对象 equals 方法为 true,则生成的 hashCode 相等」。显然 a.equals(c) 为true,按照约定 a 和 c 的 hashCode 应该相等。但我们的前提是 a 的 hashCode 不改变,因此 a 和 c 的 hashCode 并不相等,前后矛盾。

如果改变 hashCode ,那么就违反了 hashCode 的第一条约定「两个对象相等,hashCode 也相等」。例如我们通过如下代码让 a 和 c 为同一个对象,此时的 a == c。

1
2
String a = "123";
String c = a;

如果此时 a 改变了值,hashCode 也会跟着变化。但是 a == c 依然为 true,因为他们所引用的内存地址都是相同的。hashCode 规定对象相等返回相同的 hashCode ,而我们的前提是值改变,hashCode 改变,也是前后矛盾。

通过反证法,证明了 String 必须是不可变的,才能达到存储安全的目的。

缺点

每当想修改字符串的时候,都必须创建新的对象来维持它的不可变性。比如下面这个循环,循环 10 次,就创建了 10 个对象,当循环次数上升到一定程度,就会给内存管理带来巨大的压力。

1
2
3
4
String text = "0";
for (int i = 0; i < 10; i++) {
text = text + i;
}

注意事项

当我们想创建值相同的两个 String 对象,有以下三种方式

1
2
3
4
5
6
7
8
9
10
11
// 方法一
String a = "123";
String b = "123";

// 方法二
String a = new String("123");
String b = new String("123");

// 方法三
String a = "123";
String b = a;

我们分别用 == 和 equals 方法来判断 a,b是否相等,三种方式输出的结果分别会是什么呢?

方法一,a,b 的值都是 “123”,因此很简单,a.equals(b)肯定为 true,那么 a == b 呢?答案是 true。因为有「字符串常量池」的存在,当 a 创建出来,字符串常量池创建 “123” 这个字符串常量,当 b 赋值的时候,发现字符串常量池中已经有相同的对象,因此直接让 b 指向这个对象,此时 a 和 b 都指向同一个对象。如图

image.png

方法二,a 和 b 都是通过 new 方法,各自声明了一个值为 “123” 的对象,因此这是两个不同的对象。所以,a == b 为 false,a.equals(b) 为 true。此时的 a 和 b 是指向不同的对象的,虽然他们的值都是 123。

image.png

方法三,a 赋值 b,把 a 的地址指向给 b一份。因此,a,b 两个变量都是引用同一个对象,因此 a==b 为 true,a.equals(b) 也为 true。图中就是 a 指向的内存地址是 0x45,然后通过 String b = a b 得到 a 传来的地址,所以也指向 0x45 这个内存。

image.png

StringBuilder 与 StringBuffer

虽然 String 是不可变的,但是在实际生产中我们还是需要可变字符串,这要怎么解决呢?就拿上面那段循环代码来说,每次都要创建一个新的变量,循环次数多了必然占用过多内存。**StringBuilder **就是个可变字符串,因此我们可以使用 StringBuilder 来改进这段代码,这样就避免了创建多个对象。

1
2
3
4
StringBuilder text = new StringBuilder("0");
for (int i = 0; i < 10; i++) {
text = text.append(i);
}

StringBuffer 也是可变字符串,与 StringBuilder 的区别是:StringBuilder 线程不安全,但是速度快;StringBuffer 线程安全,速度相对较慢。因此,要根据实际生产环境,选择合适的类,大多数情况优先使用 StringBuilder。

String,StringBuffer 和 StringBuilder 更多 API,请参考 Java 官方文档。


String
http://wszzf.top/2021/08/11/String/
作者
Greek
发布于
2021年8月11日
许可协议