编辑从字节码和 JVM 的角度解析 Java 核心类 String 的不可变特性
1. 前言
最近看到幾個有趣的關于Java核心類String的問題。
翻閱了網上的一些博客和stackoverflow,結合自己的理解做一個匯總。
2. String類是如何實現不可變的
String類的一大特點,就是使用Final類修飾符。
A class can be declared final if its definition is complete and no subclasses are desired or required.
Because a final class never has any subclasses, the methods of a final class are never overridden .
Java SE 7 官方手冊中的定義如上,如果你認為這個類已經定義完全并且不需要任何子類的話,可以將這個類聲明為Final,Final類中的方法將永遠不會被重寫。
在Java中,String是被設計成一個不可變(immutable)類,一旦創建完后,字符串本身是無法通過正常手段被修改的。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | private final char value[];????? // 一旦初始化后,引用不能被修改 public String substring(int beginIndex, int endIndex) { ????????if (beginIndex < 0) { ????????????throw new StringIndexOutOfBoundsException(beginIndex); ????????} ????????if (endIndex > value.length) { ????????????throw new StringIndexOutOfBoundsException(endIndex); ????????} ????????int subLen = endIndex - beginIndex; ????????if (subLen < 0) { ????????????throw new StringIndexOutOfBoundsException(subLen); ????????} ????????return ((beginIndex == 0) && (endIndex == value.length)) ? this ????????????????: new String(value, beginIndex, subLen); ????} |
選了substring方法來做一個代表,其他常見的涉及String操作的方法都是類似,如果你操作后的內容會和目前String中的內容不一致的話,那么都是重新創建一個新的String類返還,不會讓你去修改內部的內容。
將String類設計成Final類,能夠避免其方法被子類重寫,從而破壞了它本身方法的實現,進而破壞了不可變的特性。
2.1 String類設計成不可變的好處
我們都不是Java語言的設計者,不知道其為何一定要設計成不可變,試著做一些猜想。
這是結合個人理解和stackoverflow上看的匯總,我們來看看Java語言的爸爸James Gosling是怎么說的。
From a strategic point of view, they tend to more often be trouble free. And there are usually things you can do with immutables that you can’t do with mutable things, such as cache the result. If you pass a string to a file open method, or if you pass a string to a constructor for a label in a user interface, in some APIs (like in lots of the Windows APIs) you pass in an array of characters. The receiver of that object really has to copy it, because they don’t know anything about the storage lifetime of it. And they don’t know what’s happening to the object, whether it is being changed under their feet.
You end up getting almost forced to replicate the object because you don’t know whether or not you get to own it. And one of the nice things about immutable objects is that the answer is, “Yeah, of course you do.” Because the question of ownership, who has the right to change it, doesn’t exist.
One of the things that forced Strings to be immutable was security. You have a file open method. You pass a String to it. And then it’s doing all kind of authentication checks before it gets around to doing the OS call. If you manage to do something that effectively mutated the String, after the security check and before the OS call, then boom, you’re in. But Strings are immutable, so that kind of attack doesn’t work. That precise example is what really demanded that Strings be immutable.
這是James Gosling在2001年5月的一次訪談中,談到了不可變類和String,大意就是 他會更傾向于使用不可變類,它能夠緩存結果,當你在傳參的時候,使用不可變類不需要去考慮誰可能會修改其內部的值,這個問題不存在的。如果使用可變類的話,可能需要每次記得重新拷貝出里面的值,性能會有一定的損失。
老爺子還說了,迫使String類設計成不可變的另一個原因是安全,當你在調用其他方法,比如調用一些系統級操作之前,可能會有一系列校驗,如果是可變類的話,可能在你校驗過后,其內部的值被改變了,可能引起嚴重的系統崩潰問題,這是迫使String類設計成不可變類的重要原因。
2.2 String Pool
上文說了,設計成不可變后,可以多個變量引用JVM上同一塊地址,可以節省內存空間,相同的字符串不用重復占用Heap區域空間。
| 1 2 | String test1 = "abc"; String test2 = "abc"; |
通常我們平時在使用字符串是,都是通過這種方式使用,那么JVM中的大致存儲就是如下圖所示。
兩個變量同時引用了String Pool中的abc,如果String類是可變的話,也就不能存在String Pool這樣的設計了。 在平時我們還會通過new關鍵字來生成String,那么新創建的String是否也會和上文中的示例一樣共享同一個字符串地址呢。
| 1 2 3 | String test1 = "abc"; String test2 = "abc"; String test3 = new String("abc"); |
答案是不會,使用new關鍵字會在堆區在創建出一個字符串,所以使用new來創建字符串還是很浪費內存的,內存結構如下圖所示。
2.3 不推薦使用+來拼裝字符串的原因。
首先我們來看這一段代碼,應該是之前寫代碼比較常見的。
| 1 2 3 | String test1 = "abc"; String test2 = "abc"; String test3 = test1 + test2; |
test3通過test1和test2拼接而成,我們看一下這個過程中的字節碼。

從以上圖我們可以看到,目前的JDK7的做法是,會通過新建StringBuilder的方式來完成這個+號的操作。這是目前的一個底層字節碼的實現,那么是不是沒有使用StringBuilder或者StringBuffer的必要了呢。還是有的,看下一個例子。
| 1 2 3 4 5 6 | String test2 = "abc"; String test3 = "abc"; for (int i = 0; i < 5; i++) { ????test3 += test2; } |
在上述代碼中,我們還是使用+號進行拼接,但這次我們加了一個循環,看一下字節碼有什么變化。 
每次循環都會創建一個StringBuilder,在末尾再調用toString返還回去,效率很低。繼續看下一個例子,我們直接使用StringBuilder,來做拼接。
| 1 2 3 4 5 6 | String test2 = "abc"; // 使用StringBuilder進行拼接 StringBuilder test4 = new StringBuilder("abc"); for (int i = 0; i < 5; i++) { ????test4.append(test2); } |
每次循環體中只會調用之前創建的StringBuilder的append方法進行拼接,效率大大提高。
至于StringBuilder 的內部實現,諸位有興趣可以自己再去看一下,本質上也是一個char數組上的操作,和StringBuffer的區別在于,StringBuffer是有做同步處理的,而StringBuilder沒有。
3. 總結
本文主要探討了String類設計為Final修飾和不可變類的原因,以及為何在日常工作中不推薦使用+號進行字符串拼接。
from:?kailuncen
http://www.importnew.com/26595.html
總結
以上是生活随笔為你收集整理的编辑从字节码和 JVM 的角度解析 Java 核心类 String 的不可变特性的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java synchronized 中的
- 下一篇: 深入了解 Java 之虚拟机内存