1、string常量池
當使用new string(“hello”)時,jvm會先使用常量池來管理“hello”直接量,再調用string類的構造器來建立一個新的string對象,新建立的對象被儲存在堆記憶體中。即new string(“hello”)一共産生了兩個字元串對象。
【常量池constant pool】管理在編譯時被确定并儲存在已編譯的.class檔案中的一些資料,包括關于類、方法、接口中的常量,和字元串常量。
【字元串常量池(string pool, string intern pool, string保留池)】 是java堆記憶體中一個特殊的存儲區域,
當建立一個string對象時,假如此字元串值已經存在于常量池中,則不會建立一個新的對象,而是引用已經存在的對象。
了解了下面這段代碼就把常量池了解的七七八八了吧。
public void test(){
string a ="張三";
string b ="張";
string c ="三";
string d = b + c;
system.out.println(a == d);//
false
string e ="張"+"三";
system.out.println(a == e);//
true
}
d=b+c:先執行stringbuilder的拼接,相當于new了一下,雖然值相等,但記憶體位址已變。
當java能直接使用字元串直接量(包括在編譯時就計算出來的字元串值時,如string e = "張" + "三";),jvm就會使用常量池來管理這些字元串。
2、string為什麼是不可變的?
注:【以下内容來于網絡,并結合自己的了解】
答案一:
最流行的java面試題之一就是:什麼是不可變對象(immutable object),不可變對象有什麼好處,在什麼情況下應該用,或者更具體一些,java的string類為什麼要設成immutable類型?
不可變對象,顧名思義就是建立後不可以改變的對象,典型的例子就是java中的string類。
string s = "abc";
s.tolowercase();
如上s.tolowercase()并沒有改變“abc“的值,而是建立了一個新的string類“abc”,然後将新的執行個體的指向變量s。
相對于可變對象,不可變對象有很多優勢:
1).不可變對象可以提高string pool的效率和安全性。如果你知道一個對象是不可變的,那麼需要拷貝這個對象的内容時,就不用複制它的本身而隻是複制它的位址,複制位址(通常一個指針的大小)需要很小的記憶體效率也很高。對于同時引用這個“abc”的其他變量也不會造成影響。
2).不可變對象對于多線程是安全的,因為在多線程同時進行的情況下,一個可變對象的值很可能被其他程序改變,這樣會造成不可預期的結果,而使用不可變對象就可以避免這種情況。
當然也有其他方面原因,但是java把string設成immutable最大的原因應該是效率和安全。
答案二:
這是一個老生常談的話題(this is an old yet still popular question). 在java中将string設計成不可變的是綜合考慮到各種因素的結果,想要了解這個問題,需要綜合記憶體,同步,資料結構以及安全等方面的考慮. 在下文中,我将為各種原因做一個小結。
1. 字元串常量池的需要
字元串常量池(string pool, string intern pool, string保留池) 是java堆記憶體中一個特殊的存儲區域, 當建立一個string對象時,假如此字元串值已經存在于常量池中,則不會建立一個新的對象,而是引用已經存在的對象。
如下面的代碼所示,将會在堆記憶體中隻建立一個實際string對象.
string s1 = "abcd";
string s2 = "abcd";
示意圖如下所示:
假若字元串對象允許改變,那麼将會導緻各種邏輯錯誤,比如改變一個對象會影響到另一個獨立對象. 嚴格來說,這種常量池的思想,是一種優化手段.
請思考: 假若代碼如下所示,s1和s2還會指向同一個實際的string對象嗎?
代碼如下:
public class test
{
public static void main(string[] args)
string s1 = "ab" + "cd";
string s2 = "abc" + "d";
system.out.println("s1==s2:" + s1 == s2);
}
// output:false
(注意和下面代碼對比)
也許這個問題違反新手的直覺, 但是考慮到現代編譯器會進行正常的優化, 是以他們都會指向常量池中的同一個對象. 或者,你可以用 jd-gui 之類的工具檢視一下編譯後的class檔案.(自帶javap -c 指令)
編譯後檔案如下:
public class test {
public test();
code:
0: aload_0
1: invokespecial
#8 //method
java/lang/object."<init>":()v
4: return
public static void main(java.lang.string[]);
0: ldc #16 //string
abcd
2: astore_1
3: ldc #16 //string
5: astore_2
6: getstatic #18 //field
java/lang/system.out:ljava/io/printstream;
9: new #24 //class
java/lang/stringbuilder
12: dup
13: ldc #26 //string
s1==s2:
15: invokespecial
#28 //method
java/lang/stringbuilder."<init>":(ljava/lang/string;)v
18: aload_1
19: invokevirtual
#31 //method
java/lang/stringbuilder.append:(ljava/lang/string;)ljava/lang/stringbuilder;
22: invokevirtual
#35 //method
java/lang/stringbuilder.tostring:()ljava/lang/string;
25: aload_2
26: if_acmpne 33
29: iconst_1
30: goto 34
33: iconst_0
34: invokevirtual
#39 //method
java/io/printstream.println:(z)v
37: return
public test();
code:
0: aload_0
1: invokespecial
#8 //method
java/lang/
// object."<init>":()v
4: return
public static void main(java.lang.string[]);
0: ldc #16 //
string abcd
2: astore_1
3: ldc #16 //
5: astore_2
6: getstatic #18 //
field java/lang/
// system.out:ljava/io/printstream;
9: aload_1
10: aload_2
11: if_acmpne 18
14: iconst_1
15: goto 19
18: iconst_0
19: invokevirtual
#24 //method
java/io/
// printstream.println:(z)v
22: return
// 位元組碼指令2
兩個“abcd”都是#16,多定義了一個s0=“123”後發現兩個“abcd”都是#30【這就是代表指向同一個對象?】
接下來高潮來了,上面的代碼稍稍改一下:
system.out.println(s1 == s2);
// output:true
// 代碼僅僅少了一個字元串而已
編譯一下:見【位元組碼指令2】左邊有底色的是前者比後者的多的位元組碼指令部分。
兩者有什麼差別呢?
最初還以為是在底層實作有差別,後經導師指點,僅僅是運算符優先級【+優先級高于==,詳見筆記《運算符優先級表》】的差別,前者是先計算+号,執行拼接,再和後面的s2比較,肯定是false啊。當然可以把後面加個括号,輸出就變為true了,而且是 "s1==s2:"true 。
了解了這個,再稍微改動一下;
string s1 = "ab";
string s2 = "abc" + "d";
system.out.println(s1 + "cd" == s2);
輸出為:false 【隻有false,沒有前面"s1==s2:"這一串】
// 先執行stringbuilder的拼接,相當于new了一下,雖然值相等,但記憶體位址已變。
但如果這樣寫:
string s1 = "1234";
string s01 = "123";
string s02 = "4";
string s2 = s01 + s02;
system.out.println(s1 == s2);
// s2取了s01、s02的引用位址。肯定和s1不同了額。
引:
hashcode()傳回的是jvm中位址的哈希碼,而不是jvm中的位址,要想得到str在實體記憶體中的真實地存,那隻有用jni技術調用c/c++去實作,否則無能為力,因為java超不出jvm,而jvm對實體記憶體位址是“不可見”的,否則java中不就有了指針,而去直接操作記憶體了,當然這是與java語言相違背的。這些隻是我個人見解,說不定還真有高手直接用java語言得到了實體記憶體中的位址了呢。s1.getbytes()也不行。
2. 允許string對象緩存hashcode
java中string對象的哈希碼被頻繁地使用, 比如在hashmap 等容器中。
字元串不變性保證了hash碼的唯一性,是以可以放心地進行緩存.這也是一種性能優化手段,意味着不必每次都去計算新的哈希碼. 在string類的定義中有如下代碼:
private int hash;//用來緩存hashcode
3. 安全性
string被許多的java類(庫)用來當做參數,例如 網絡連接配接位址url,檔案路徑path,還有反射機制所需要的string參數等, 假若string不是固定不變的,将會引起各種安全隐患。
假如有如下的代碼:
boolean connect(string s){
if (!issecure(s)) {
throw new securityexception();
// 如果在其他地方可以修改string,那麼此處就會引起各種預料不到的問題/錯誤
causeproblem(s);
總體來說, string不可變的原因包括 設計考慮,效率優化問題,以及安全性這三大方面. 事實上,這也是java面試中的許多 "為什麼" 的答案。
答案三:string類不可變性的好處
string是所有語言中最常用的一個類。我們知道在java中,string是不可變的、final的。java在運作時也儲存了一個字元串池(string pool),這使得string成為了一個特别的類。
string類不可變性的好處
1.隻有當字元串是不可變的,字元串池才有可能實作。字元串池的實作可以在運作時節約很多heap(堆)空間,因為不同的字元串變量都指向池中的同一個字元串。但如果字元串是可變的,那麼string
interning将不能實作(譯者注:string interning(拘留)是指對不同的字元串僅僅隻儲存一個,即不會儲存多個相同的字元串。),因為這樣的話,如果變量改變了它的值,那麼其它指向這個值的變量的值也會一起改變。
2.如果字元串是可變的,那麼會引起很嚴重的安全問題。譬如,資料庫的使用者名、密碼都是以字元串的形式傳入來獲得資料庫的連接配接,或者在socket程式設計中,主機名和端口都是以字元串的形式傳入。因為字元串是不可變的,是以它的值是不可改變的,否則黑客們可以鑽到空子,改變字元串指向的對象的值,造成安全漏洞。
3.因為字元串是不可變的,是以是多線程安全的,同一個字元串執行個體可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字元串自己便是線程安全的。
4.類加載器要用到字元串,不可變性提供了安全性,以便正确的類被加載。譬如你想加載java.sql.connection類,而這個值被改成了myhacked.connection,那麼會對你的資料庫造成不可知的破壞。
5.因為字元串是不可變的,是以在它建立的時候hashcode就被緩存了,不需要重新計算。這就使得字元串很适合作為map中的鍵,字元串的處理速度要快過其它的鍵對象。這就是hashmap中的鍵往往都使用字元串。
以上就是我總結的字元串不可變性的好處。
什麼是不可變對象?
string對象是不可變的,但這僅意味着你無法通過調用它的公有方法來改變它的值。
衆所周知, 在java中, string類是不可變的。那麼到底什麼是不可變的對象呢? 可以這樣認為:如果一個對象,在它建立完成之後,不能再改變它的狀态,那麼這個對象就是不可變的。不能改變狀态的意思是,不能改變對象内的成員變量,包括基本資料類型的值不能改變,引用類型的變量不能指向其他的對象,引用類型指向的對象的狀态也不能改變。
區分對象和對象的引用
對于java初學者, 對于string是不可變對象總是存有疑惑。看下面代碼:
<code>string s = </code><code>"abcabc"</code><code>;</code>
<code>system.out.println(</code><code>"s = "</code><code>+ s);</code>
<code>s = </code><code>"123456"</code><code>;</code>
列印結果為:
<code>s = abcabc</code>
<code></code><code>s = 123456</code>
首先建立一個string對象s,然後讓s的值為“abcabc”, 然後又讓s的值為“123456”。 從列印結果可以看出,s的值确實改變了。那麼怎麼還說string對象是不可變的呢? 其實這裡存在一個誤區: s隻是一個string對象的引用,并不是對象本身。對象在記憶體中是一塊記憶體區,成員變量越多,這塊記憶體區占的空間越大。引用隻是一個4位元組的資料,裡面存放了它所指向的對象的位址,通過這個位址可以通路對象。
也就是說,s隻是一個引用,它指向了一個具體的對象,當s=“123456”; 這句代碼執行過之後,又建立了一個新的對象“123456”, 而引用s重新指向了這個新的對象,原來的對象“abcabc”還在記憶體中存在,并沒有改變。
java和c++的一個不同點是, 在java中不可能直接操作對象本身,所有的對象都由一個引用指向,必須通過這個引用才能通路對象本身,包括擷取成員變量的值,改變對象的成員變量,調用對象的方法等。而在c++中存在引用,對象和指針三個東西,這三個東西都可以通路對象。其實,java中的引用和c++中的指針在概念上是相似的,他們都是存放的對象在記憶體中的位址值,隻是在java中,引用喪失了部分靈活性,比如java中的引用不能像c++中的指針那樣進行加減運算。
為什麼string對象是不可變的?
要了解string的不可變性,首先看一下string類中都有哪些成員變量。 在jdk1.6中,string的成員變量有以下幾個:
<code>public </code><code>final </code><code>class </code><code>string</code>
<code></code><code> implements</code><code>java.io.serializable, comparable<string>, charsequence</code>
<code>{</code>
<code></code><code>/** the value is used for character storage. */</code>
<code></code><code> private </code><code>final </code><code>char </code><code>value[];</code>
<code></code><code>/** the offset is the first index of the storage that is used. */</code>
<code></code><code> private </code><code>final </code><code>int </code><code>offset;</code>
<code></code><code>/** the count is the number of characters in the string. */</code>
<code></code><code> private </code><code>final </code><code>int </code><code>count;</code>
<code></code><code>/** cache the hash code for the string */</code>
<code></code><code> private </code><code>int </code><code>hash; </code><code>// default to 0</code>
在jdk1.8中,string類做了一些改動,主要是改變了substring方法執行時的行為,這和本文的主題不相關。jdk1.8中string類的主要成員變量就剩下了兩個:
public final class string
implements java.io.serializable,
comparable<string>, charsequence {
/**
the value is used for character storage. */
private final charvalue[];
cache the hash code for the string */
private int hash; //
default to 0
* class string is special
cased within the serialization stream protocol.
*
* a string instance is
written into an objectoutputstream according to
* <a href="{@docroot}/../platform/serialization/spec/output.html">
* object serialization
specification, section 6.2, "stream elements"</a>
*/
// 聲明序列化時要包含的域,僅僅聲明string中未使用
private static final objectstreamfield[] serialpersistentfields =
new objectstreamfield[0];
由以上的代碼可以看出, 在java中string類其實就是對字元數組的封裝。jdk6中, value是string封裝的數組,offset是string在這個value數組中的起始位置,count是string所占的字元的個數。在jdk7中,隻有一個value變量,也就是value中的所有字元都是屬于string這個對象的。這個改變不影響本文的讨論。 除此之外還有一個hash成員變量,是該string對象的哈希值的緩存,這個成員變量也和本文的讨論無關。在java中,數組也是對象(可以參考我之前的文章java中數組的特性)。
是以value也隻是一個引用,它指向一個真正的數組對象。其實執行了string s = “abcabc”; 這句代碼之後,真正的記憶體布局應該是這樣的:
value,offset和count這三個變量都是private的,并且沒有提供setvalue, setoffset和setcount等公共方法來修改這些值,是以在string類的外部無法修改string。也就是說一旦初始化就不能修改, 并且在string類的外部不能通路這三個成員。此外,value,offset和count這三個變量都是final的, 也就是說在string類内部,一旦這三個值初始化了, 也不能被改變。是以可以認為string對象是不可變的了。
那麼在string中,明明存在一些方法,調用他們可以得到改變後的值。這些方法包括substring, replace, replaceall, tolowercase等。例如如下代碼:
<code>string a = </code><code>"abcabc"</code><code>;</code>
<code>system.out.println(</code><code>"a = "</code><code>+ a);</code>
<code>a = a.replace(</code><code>'a'</code><code>, </code><code>'a'</code><code>);</code>
<code>a = abcabc</code>
那麼a的值看似改變了,其實也是同樣的誤區。再次說明, a隻是一個引用, 不是真正的字元串對象,在調用a.replace(‘a', ‘a')時, 方法内部建立了一個新的string對象,并把這個心的對象重新賦給了引用a。string中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);
}
return this;
讀者可以自己檢視其他方法,都是在方法内部重新建立新的string對象,并且傳回這個新的對象,原來的對象是不會被改變的。這也是為什麼像replace, substring,tolowercase等方法都存在傳回值的原因。也是為什麼像下面這樣調用不會改變對象的值:
<code>string ss = </code><code>"123456"</code><code>;</code>
<code>system.out.println(</code><code>"ss = "</code><code>+ ss);</code>
<code>ss.replace(</code><code>'1'</code><code>, </code><code>'0'</code><code>);</code>
列印結果:
<code>ss = </code><code>123456</code>
<code></code><code>ss = </code><code>123456</code>
string對象真的不可變嗎?
從上文可知string的成員變量是private final 的,也就是初始化之後不可改變。那麼在這幾個成員中, value比較特殊,因為他是一個引用變量,而不是真正的對象。value是final修飾的,也就是說final不能再指向其他數組對象,那麼我能改變value指向的數組嗎? 比如将數組中的某個位置上的字元變為下劃線“_”。 至少在我們自己寫的普通代碼中不能夠做到,因為我們根本不能夠通路到這個value引用,更不能通過這個引用去修改數組。
那麼用什麼方式可以通路私有成員呢? 沒錯,用反射, 可以反射出string對象中的value屬性,
進而通過改變獲得的value引用改變數組的結構。下面是執行個體代碼:
import java.lang.reflect.field;
public class reflection
public static void main(string[] args) throws exception
// 建立字元串"hello
world",并賦給引用s
string s = "hello
world";
system.out.println("原 s=
" + s + "
,hash:" + s.hashcode()); // 原s=
hello world 位址:-862545276
commonchange(s);
system.out.println("======反射修改======");
// 擷取string類中的value字段
field valuefield =
string.class.getdeclaredfield("value");
// 改變value屬性的通路權限
valuefield.setaccessible(true);
// 擷取s對象上的value屬性的值
char[] value =
(char[]) valuefield.get(s);
// 改變value所引用的數組中的第5個字元
//
value[5] = '_';
valuefield.set(s, newchar[]{'h', 'a', 'p', 'p', 'y'});
system.out.println("反射s=
,hash:" + s.hashcode()); // 新s=
happy 位址:-862545276
private static void commonchange(string s)
system.out.println("======普通修改======");
s = "hello--world";
system.out.println("改變s=
,hash:" + s.hashcode()); // 改變s=
hello--world ,hash:753841376
string newstr = new string("hello--world");
system.out.println("新 s=
" + newstr + "
,hash:" + newstr.hashcode()); // 新 s=
hello--world ,hash:753841376【記憶體中已存在】
system.out.println(s == newstr); //
依舊 false 【雖然hash相同】
在這個過程中,s始終引用的同一個string對象,但是在反射前後,這個string對象發生了變化, 也就是說,通過反射是可以修改所謂的“不可變”對象的。但是一般我們不這麼做。這個反射的執行個體還可以說明一個問題:如果一個對象,他組合的其他對象的狀态是可以改變的,那麼這個對象很可能不是不可變對象。例如一個car對象,它組合了一個wheel對象,雖然這個wheel對象聲明成了private final 的,但是這個wheel對象内部的狀态可以改變, 那麼就不能很好的保證car對象不可變。
參考:http://www.jb51.net/article/73243.htm