本文章還是得提到String Immutable 特性。這樣才有利於String 、StringBuffer 、StringBuilder 特點的比較。
以下基於 java for windows
java version “1.8.0_171”
Java™ SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot™ 64-Bit Server VM (build 25.171-b11, mixed mode)圖中示例代碼註釋只是爲了展示、對比方便,所以放在代碼尾部
String
String Immutable 特性
以下爲 java.lang.String 屬性字段
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
String 類 implements java.io.Serializable, Comparable< String >, CharSequence。類是用final修飾的。且其屬性字段爲value未提供set修改其對象內部值。首先要了解一點
-
每次新建String類型對象 其值(方法區 String Pool 、堆區) 都是不可變的(immutable)。最直觀代碼如下
// 這種行爲是在 String pool 創建對象,其引用存在棧中 String str1 = "hello world"; // 使用此種方法重新創建String對象時 java會首先在String pool中檢查是否存在 hello world 存在即返回其引用 所以 str1、str2 指向同一對象 String str2 = "hello world"; System.out.println("result:" + (str1 == str2)); // result:true
// 此種方式創建的String對象位於堆內 每次創建都指向一個新的對象 各自獨立的堆空間 String str3 = new String("hello world"); String str4 = new String("hello world"); System.out.println("result2:" + (str3 == str4));// result2:false // 在堆中的String不同於在String pool 的String System.out.println("result3:" + (str2 == str4)); // result3:false
// 此操作在Pool中重新創建String 其引用改變 但原 hello world 未變 str1 = "hello Man"; System.out.println("change1: " + str1);// change1: hello Man System.out.println("change1: " + str2);// change1: hello world
-
String immutable特性帶來很多便利
- 不可變對象可以提高String Pool的效率和安全性。如果你知道一個對象是不可變的,那麼需要拷貝這個對象的內容時,就不用複製它的本身而只是複製它的地址,複製地址(通常一個指針的大小)需要很小的內存效率也很高。對於同時引用這個“hello world” 的其他變量也不會造成影響。節省空間,加快效率。
- 不可變對象對於多線程是安全的,因爲在多線程同時進行的情況下,一個可變對象的值很可能被其他進程改變,這樣會造成不可預期的結果,而使用不可變對象就可以避免這種情況。(最直觀的例子就是 String被廣泛的作爲方法的參數,比如打開網絡連接時傳遞主機名和端口號作爲參數,傳遞數據庫URL用於連接數據庫,也可以在打開文件時傳遞文件名給Java的I/O class。如果String不是immutable,會帶來嚴重的安全問題,假設一個人對其授權訪問的文件,改變了文件名並重新獲了文件訪問權限,但是因爲String是immutable,你就不需要擔心這種情況。這也可以解釋爲啥String 是final的,final string保證了任何人都不能重寫String而改變String的行爲。)
- 不可變性,導致其是線程安全的。
- 支持hash映射和緩存(此點在之後的文章中會提到)
反射修改了String的值?
在 JAVA不可變類(immutable)機制與String的不可變性 中 看到博主的一段說明:
發現String的值已經發生了改變。也就是說,通過反射是可以修改所謂的“不可變”對象的
特以下代碼驗證此點
String str1 = "hello world";
String str2 = "hello world";
System.out.println("result1:" + (str1 == str2)); // result1:true
//獲取String類中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改變value屬性的訪問權限
valueFieldOfString.setAccessible(true);
//獲取s對象上的value屬性的值
char[] value = (char[]) valueFieldOfString.get(str1);
//改變value所引用的數組中的第5個字符
value[5] = 'X';
System.out.println("str1 changed = " + str1); // str1 changed = helloXworld
System.out.println("str2 = " +str2);// str2 = helloXworld
結果顯而易見:改了裏面的值,並且執行其值的 str2 值也被修改
但是,如果將 str1 使用final修飾,輸出的結果將是:
str1 changed = hello world
str2 = helloXworld
也就是說 str1 的值並未改變
爲什麼會出現這種情況?請見下文。
這是我的java類 .java(str1未被final修飾前)
import java.lang.reflect.Field;
/**
* @Description:
* @Author: amarone
*/
public class Test {
public static void main(String[] args) throws Exception{
String str1 = "hello world";
String str2 = "hello world";
System.out.println("result1:" + (str1 == str2));
Field valueFieldOfString = String.class.getDeclaredField("value");
valueFieldOfString.setAccessible(true);
char[] value = (char[]) valueFieldOfString.get(str1);
value[5] = 'X';
System.out.println("str1 changed = " + str1); //str1 changed = helloXworld
System.out.println("str2 = " +str2); // str2 = helloXworld
}
}
這是經過javac 編譯.java 後的 .class 。 通過 jd-gui.exe 反編譯 (str1未被final修飾前)
import java.io.PrintStream;
import java.lang.reflect.Field;
public class Test
{
public static void main(String[] paramArrayOfString)
throws Exception
{
String str1 = "hello world";
String str2 = "hello world";
System.out.println("result1:" + (str1 == str2));
Field localField = String.class.getDeclaredField("value");
localField.setAccessible(true);
char[] arrayOfChar = (char[])localField.get(str1);
arrayOfChar[5] = 'X';
System.out.println("str1 changed = " + str1);
System.out.println("str2 = " + str2);
}
}
這是經過javac 編譯.java 後的 .class 。 通過 jd-gui.exe 反編譯 (str1被final修飾)
import java.io.PrintStream;
import java.lang.reflect.Field;
public class Test
{
public static void main(String[] paramArrayOfString)
throws Exception
{
String str = "hello world";
System.out.println("result1:" + ("hello world" == str));
Field localField = String.class.getDeclaredField("value");
localField.setAccessible(true);
char[] arrayOfChar = (char[])localField.get("hello world");
arrayOfChar[5] = 'X';
System.out.println("str1 changed = hello world");
System.out.println("str2 = " + str);
}
}
可以注意到: 在加上final 修飾之後,java文件被編譯爲class文件時
(這也是我上篇文章《(四)Final 關鍵字》 中提到部分有涉及的 編譯期優化 )
System.out.println("str1 changed = " + str1); ==> System.out.println("str1 changed = hello world");
輸出的不是引用的變量而是一個字符串!也就是說在編譯器就已經優化了,變量再怎麼改都沒用。final修飾Stirng是爲了防止繼承String暴露Stirng內部屬性。
爲什麼要有StringBuffer或者StringBuilder?
Stirng 字符串拼接時
import java.io.PrintStream;
public class Test
{
public static void main(String[] paramArrayOfString)
throws Exception
{
String str1 = "aaa";
String str2 = str1 + "b";
System.out.println(str1);
System.out.println(str2);
System.out.println(str1 + str2);
}
}
str1、str2、"b" 都是單獨的對象,如果存在大量的字符串拼接,那麼就會大量創建對象,然後你的新生代(倖存者區)很快就滿了80% 然後就觸發GC了。或者就直接掛了。
爲了應對大量拼接字符串拼接,java提供了 StringBuffer 類。
public final class StringBuffer
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
**
* A cache of the last value returned by toString. Cleared
* whenever the StringBuffer is modified.
*/
private transient char[] toStringCache;
/** use serialVersionUID from JDK 1.0.2 for interoperability */
static final long serialVersionUID = 3388685877147921107L;
········
}
StringBuffer 繼承於 AbstractStringBuilder
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
}
StringBuffer 提供了append() 方法用以在指定位置添加字符串
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
可以看到幾乎所有對字符串操作的方法全部添加了synchronized關鍵字。保證了線程安全,但是需要付出部分性能代價。