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 中,并且值并没有改变,只是暂时没有变量指向这个对象罢了。上面的操作,可以用如图所示。
了解上面的知识后,回到新的问题上来,String 是如何保证不可变性呢?
打开 String 的源代码,我们可以发现,String 类以及它的字符数组变量都是被 final 关键词修饰的。这就意味着 String 无法被继承,并且 char[] value 的地址指向无法修改,而且 String 所有的公开 API 都没有修改 char[] value 的方法,这就保证了 String 的不可变性。
优点
线程安全,不可变的对象天生就是线程安全的,可以随心所欲的在线程之间传递。
存储安全,为什么说是存储安全呢?因为遵守了 hashCode 的约定。
正是因为 Stirng 的不可变性,才能实现存储安全。我们可以想象一下有一个 Map<String,String> map = new HashMap<>();
,假如 String 是可变的话,那么就违反了 hashCode 的约定,想了解 hashCode 的相关约定访问这里 。
如图所示,有三个字符串 “a”,”c”,通过计算分别得到了对应的 hashCode。如果 String 是可变的行不行呢?我们可以通过反证法来证明。现在假如 String 是可变的,那么当字符串从 a 变成了 c(即 String a = "a" 到 a = "c"
),他的 hashCode 要不要改变呢?分两种情况。
如果不改变 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 |
|
如果此时 a 改变了值,hashCode 也会跟着变化。但是 a == c 依然为 true,因为他们所引用的内存地址都是相同的。hashCode 规定对象相等返回相同的 hashCode ,而我们的前提是值改变,hashCode 改变,也是前后矛盾。
通过反证法,证明了 String 必须是不可变的,才能达到存储安全的目的。
缺点
每当想修改字符串的时候,都必须创建新的对象来维持它的不可变性。比如下面这个循环,循环 10 次,就创建了 10 个对象,当循环次数上升到一定程度,就会给内存管理带来巨大的压力。
1 |
|
注意事项
当我们想创建值相同的两个 String 对象,有以下三种方式
1 |
|
我们分别用 == 和 equals 方法来判断 a,b是否相等,三种方式输出的结果分别会是什么呢?
方法一,a,b 的值都是 “123”,因此很简单,a.equals(b)
肯定为 true,那么 a == b
呢?答案是 true。因为有「字符串常量池」的存在,当 a 创建出来,字符串常量池创建 “123” 这个字符串常量,当 b 赋值的时候,发现字符串常量池中已经有相同的对象,因此直接让 b 指向这个对象,此时 a 和 b 都指向同一个对象。如图
方法二,a 和 b 都是通过 new 方法,各自声明了一个值为 “123” 的对象,因此这是两个不同的对象。所以,a == b 为 false,a.equals(b) 为 true。此时的 a 和 b 是指向不同的对象的,虽然他们的值都是 123。
方法三,a 赋值 b,把 a 的地址指向给 b一份。因此,a,b 两个变量都是引用同一个对象,因此 a==b 为 true,a.equals(b) 也为 true。图中就是 a 指向的内存地址是 0x45,然后通过 String b = a
b 得到 a 传来的地址,所以也指向 0x45 这个内存。
StringBuilder 与 StringBuffer
虽然 String 是不可变的,但是在实际生产中我们还是需要可变字符串,这要怎么解决呢?就拿上面那段循环代码来说,每次都要创建一个新的变量,循环次数多了必然占用过多内存。**StringBuilder **就是个可变字符串,因此我们可以使用 StringBuilder 来改进这段代码,这样就避免了创建多个对象。
1 |
|
StringBuffer 也是可变字符串,与 StringBuilder 的区别是:StringBuilder 线程不安全,但是速度快;StringBuffer 线程安全,速度相对较慢。因此,要根据实际生产环境,选择合适的类,大多数情况优先使用 StringBuilder。
String,StringBuffer 和 StringBuilder 更多 API,请参考 Java 官方文档。