字符串可以說是廣泛應(yīng)用在日常編程中,jdk從1.0就提供了String類來(lái)創(chuàng)建和操作字符串。同時(shí)它也是不可改變類(基本類型的包裝類都不可改變)的典型代表。
源碼查看(基于1.8)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[]; //這邊只是引用并不是真正對(duì)象
...
}
//首先string類創(chuàng)建的對(duì)象是不可變的(一個(gè)對(duì)象在創(chuàng)建完成后不能再改變它狀態(tài),說明是不可變的,并發(fā)程序最喜歡不可變量了),
//里面最主要的成員為char類型的數(shù)組
幾個(gè)構(gòu)造方法
//空的構(gòu)造方法 例如 String a = new String(); a為""空字符
public String() {
this.value = new char[0];
}
//帶參構(gòu)造方法 將源的hash和value賦給目標(biāo)String
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
幾個(gè)常用經(jīng)典的String類方法
1.equals
//如果引用指向的內(nèi)存值都相等 直接返回true
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
//instanceof判斷是否屬于或子類 但Stringfinal修飾不可繼承 只考慮是否為String類型
//上面說過String的成員為char數(shù)組,equals內(nèi)則比較char類數(shù)組元素是否一一相等
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
//長(zhǎng)度不相等返回false
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//從后往前單個(gè)字符判斷,如果有不相等,返回false
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
2.subString(beginIndex,endIndex)
subString這邊存在一個(gè)小插曲
在jdk1.7以前,該方法存在內(nèi)存泄漏問題。之所以存在是因?yàn)樵诖酥?,String三個(gè)參數(shù)的構(gòu)造方法是這么寫的。成員變量為這三個(gè),jdk7以后取消掉了offset和count加入了hash,雖然原來(lái)的構(gòu)造方法簡(jiǎn)潔高效但存在gc問題。所以7以后放棄了性能采取了更為保守的寫法。
/** The value is used for character storage. */
private final char value[];
/** The offset is the first index of the storage that is used. */
private final int offset;
/** The count is the number of characters in the String. */
private final int count;
...
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > count) {
throw new StringIndexOutOfBoundsException(endIndex);
}
if (beginIndex > endIndex) {
throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
}
//雖然這邊返回的是新的String對(duì)象,但構(gòu)造方法中還引用著原先的value
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
...
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
//這邊開始this.value = value; 出現(xiàn)問題,這三個(gè)個(gè)原來(lái)為String類中的三個(gè)私有成員變量,因?yàn)檫@種實(shí)現(xiàn)還在引用原先的字符串變量value[] 通過offset(起始位置)和count(字符所占個(gè)數(shù))返回一個(gè)新的字符串,這樣可能導(dǎo)致jvm認(rèn)為最初被截取的字符串還被引用就不對(duì)其gc,如果這個(gè)原始字符串很大,就會(huì)占用著內(nèi)存,出現(xiàn)內(nèi)存泄漏等gc問題。
jdk1.7以后的寫法變化
//雖然這邊還是有offse和count參數(shù) 但不是成員變量了
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
...
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);//構(gòu)造函數(shù)參數(shù)順序也有所變化
}
...
public String(char value[], int offset, int count) {
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// Note: offset or count might be near -1>>>1.
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
//新的不是引用之前的而是重新新建了一個(gè)。
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
3.hashcode
public int hashCode() {
int h = hash;
//如果hash沒有被計(jì)算過,并且字符串不為空,則進(jìn)行hashCode計(jì)算
if (h == 0 && value.length > 0) {
char val[] = value;
//計(jì)算過程
//s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
//hash賦值
hash = h;
}
return h;
}
//String重寫了Object類的hashcode方法,根據(jù)值來(lái)計(jì)算hashcode值,不過Object的該方法為native本地方法。
//該方法設(shè)計(jì)十分巧妙,它先從0判斷字符大小,如果
//hashcode其實(shí)就是散列碼 這種算法的話 同樣的字符串的值一定相等 但不同的字符串其實(shí)也有可能得到同樣的hashcode值
n=3
i=0 -> h = 31 * 0 + val[0]
i=1 -> h = 31 * (31 * 0 + val[0]) + val[1]
i=2 -> h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2]
//以字符串 "123"為例 1的的ascil碼為49 所以 "1".hashcode()的值為49,2為50...
h = 31 * (31 * (31 * 0 + val[0]) + val[1]) + val[2] = 31 * (31 * 49 + 50) + 51 = 48690
詳細(xì)算法參考這里
String的不可變性
//這邊可能存在一個(gè)疑問 s對(duì)象是否發(fā)生了改變
String s = "hello";
s = "world";
//String不可變不是在原內(nèi)存地址上修改數(shù)據(jù),而是重新指向一個(gè)新對(duì)象,新地址,所以這里的hello對(duì)象并沒有被改變。
//同樣的類似replace方法源碼中
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
return new String(buf, true);//這邊返回的是新的一個(gè)String對(duì)象 而不改變?cè)瓉?lái)的對(duì)象
}
}
return this; //如果都一樣 就指向同一個(gè)對(duì)象了
}

String對(duì)象不可變
String對(duì)象為什么不可變
String用final修飾是為了防止被繼承進(jìn)而破壞,而讓String對(duì)象不可變主要也是為了安全。
這里原來(lái)我有一個(gè)誤區(qū),僅僅認(rèn)為說final修飾了String類,所以String對(duì)象不可變,想法很天真,,final修飾類的話主要是讓類不可被繼承,final修飾基本類型變量不能對(duì)基本類型對(duì)象重新賦值,但對(duì)于引用類型的變量(如數(shù)組),它保存的只是一個(gè)引用,final只需保證這個(gè)引用的地址不改變,即一直引用同一個(gè)對(duì)象即可,但是這個(gè)對(duì)象還是能改變的。
比如 final int[] arr = {1};
arr[0] = 3; //改變arr = new int[]{3}; //因?yàn)檫@個(gè)是個(gè)新的數(shù)組 這樣改變了數(shù)組的地址 編譯不通過 除非去掉final
//String類源碼中的成員變量 (jdk1.8)
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0

String.png
思考
1.String對(duì)象是否真的不可變
可以通過反射改變
String的成員變量為final修飾,就是初始化之后不可改變,但是這幾個(gè)成員中value比較特殊,因?yàn)樗莻€(gè)引用變量而不是真正的對(duì)象,value[]是final修飾的,也就是說不能再指向其他數(shù)組對(duì)象,但是可以改變數(shù)組內(nèi)部的結(jié)構(gòu)來(lái)改變。
注:簡(jiǎn)單來(lái)說就是final修飾數(shù)組,指定數(shù)組所指向的內(nèi)存空間固定,數(shù)組內(nèi)部值還能改。因?yàn)閿?shù)組是引用類型,內(nèi)存地址是不可改變的。
實(shí)例代碼
public static void testReflection() throws Exception {
String s = "Hello,World";
System.out.println("s = " + s); //Hello World
//獲取String類中的value字段
Field valueOfString = String.class.getDeclaredField("value");
//改變value屬性的訪問權(quán)限
valueOfString.setAccessible(true);
//獲取s對(duì)象上的value屬性的值
char[] value = (char[]) valueOfString.get(s);
//改變value所引用的數(shù)組中的第5個(gè)字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
}
String為什么要設(shè)計(jì)成不可變
1.允許String對(duì)象緩存hashcode:
Java中String對(duì)象的哈希碼被頻繁地使用, 比如在hashMap 等容器中。字符串不變性保證了hash碼的唯一性,因此可以放心地進(jìn)行緩存。
2.安全性
String被許多的Java類(庫(kù))用來(lái)當(dāng)做參數(shù),例如 網(wǎng)絡(luò)連接地址URL,文件路徑path,還有反射機(jī)制所需要的String參數(shù)等, 假若String不是固定不變的,將會(huì)引起各種安全隱患。
對(duì)于一個(gè)方法而言,參數(shù)是為該方法提供信息的,而不是讓方法改變自己。
3.字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) java堆內(nèi)存放了一個(gè)特殊的區(qū)域用于常量池, 當(dāng)創(chuàng)建一個(gè)String對(duì)象時(shí),假如此字符串值已經(jīng)存在于常量池中,則不會(huì)創(chuàng)建一個(gè)新的對(duì)象,而是引用已經(jīng)存在的對(duì)象。