初探Java字符串

作者:蔡曉建
原文地址:
http://mccxj.github.io/blog/20130615_java-string-constant-pool.html

String印象

String是java中的無處不在的類姚糊,使用也很簡單稀蟋。初學(xué)java阔加,就已經(jīng)有字符串是不可變的蓋棺定論旭斥,解釋通常是:它是final的篡石。

不過阎毅,String是有字面量這一說法的塔橡,這是其他類型所沒有的特性(除原生類型)佳镜。另外,java中也有字符串常量池這個說法姊氓,用來存儲字符串字面量丐怯,不是在堆上,而是在方法區(qū)里邊存在的翔横。

字面量和常量池初探

字符串對象內(nèi)部是用字符數(shù)組存儲的读跷,那么看下面的例子:

String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");

這些語句會發(fā)生什么事情? 大概是這樣的:

  1. 會分配一個11長度的char數(shù)組,并在常量池分配一個由這個char數(shù)組組成的字符串禾唁,然后由m去引用這個字符串效览。
  2. 用n去引用常量池里邊的字符串,所以和n引用的是同一個對象荡短。
  3. 生成一個新的字符串丐枉,但內(nèi)部的字符數(shù)組引用著m內(nèi)部的字符數(shù)組。
  4. 同樣會生成一個新的字符串掘托,但內(nèi)部的字符數(shù)組引用常量池里邊的字符串內(nèi)部的字符數(shù)組瘦锹,意思是和u是同樣的字符數(shù)組。

如果我們使用一個圖來表示的話闪盔,情況就大概是這樣的(使用虛線只是表示兩者其實沒什么特別的關(guān)系):

對象在內(nèi)存中的布局

結(jié)論就是,m和n是同一個對象弯院,但m,u,v都是不同的對象,但都使用了同樣的字符數(shù)組锭沟,并且用equal判斷的話也會返回true抽兆。

我們可以使用反射修改字符數(shù)組來驗證一下效果,可以試試下面的測試代碼:

@Test
public void test1() throws Exception {
    String m = "hello,world";
    String n = "hello,world";
    String u = new String(m);
    String v = new String("hello,world");

    Field f = m.getClass().getDeclaredField("value");
    f.setAccessible(true);
    char[] cs = (char[]) f.get(m);
    cs[0] = 'H';

    String p = "Hello,world";
    Assert.assertEquals(p, m);
    Assert.assertEquals(p, n);
    Assert.assertEquals(p, u);
    Assert.assertEquals(p, v);
}

從上面的例子可以看到族淮,經(jīng)常說的字符串是不可變的辫红,其實和其他的final類還是沒什么區(qū)別,還是引用不可變的意思祝辣。 雖然String類不開放value贴妻,但同樣是可以通過反射進(jìn)行修改,只是通常沒人這么做而已蝙斜。 即使是涉及”修改”的方法名惩,都是通過產(chǎn)生一個新的字符串對象來實現(xiàn)的,例如replace孕荠、toLower娩鹉、concat等。 這樣做的好處就是讓字符串是一個狀態(tài)不可變類稚伍,在多線程操作時沒有后顧之憂弯予。

當(dāng)然,在字符串修改的時候个曙,會產(chǎn)生一個新的對象锈嫩,如果執(zhí)行很頻繁,就會導(dǎo)致大量對象的創(chuàng)建,性能問題也就隨之而來了呼寸。 為了應(yīng)付這個問題艳汽,通常我們會采用StringBufferStringBuilder類來處理。

另外对雪,字符串常量通常是在編譯的時候就確定好的河狐,定義在類的方法區(qū)里邊,也就是說慌植,不同的類甚牲,即使用了同樣的字符串, 還是屬于不同的對象蝶柿。所以才需要通過引用字符串常量來減少相同的字符串的數(shù)量丈钙。可以通過下面的代碼來測試一下:

class A {
    public void print() {
        System.out.println("hello");
    }
}

class B {
    public void print() {
        String s = "hello";
        // 修改s的第一個字符為H
        System.out.println("hello"); // 輸出Hello
        new A().print(); // 輸出hello
    }
}

字符串操作細(xì)節(jié)

String類內(nèi)部處理有個字符數(shù)組之外交汤,還使用偏移位置offset和長度count雏赦, 通過offset和count來確定字符數(shù)組的一部分,這部分才是這個字符串的真正的內(nèi)容芙扎。 例如星岗,有substring這個常用方法,看下面的例子:

String m = "hello,world";
String u = m.substring(2,10);
String v = u.substring(4,7);

按照上面的說法戒洼,m,n的數(shù)據(jù)結(jié)構(gòu)就如下圖所示:

substring在內(nèi)存中的布局

可以發(fā)現(xiàn)俏橘,m,n,v是三個不同的字符串對象,但引用的value數(shù)組其實是同一個圈浇。 同樣可以通過上述反射的代碼進(jìn)行驗證寥掐,這里就不詳述了。

但字符串操作時磷蜀,可能需要修改原來的字符串?dāng)?shù)組內(nèi)容或者原數(shù)組沒法容納的時候召耘,就會使用另外一個新的數(shù)組,例如replace,concat,+等操作褐隆。另外污它,oracle的JDK實現(xiàn)中,String的構(gòu)造方法庶弃,對于字符串參數(shù)只是引用部分字符數(shù)組的情況(count小于字符數(shù)組長度)衫贬,采用的是拷貝新數(shù)組的方式,是比較特別的歇攻,不過這個構(gòu)造方法也沒什么機(jī)會使用到固惯。

例如下面的代碼:

String m = "hello,";
String u = m.concat("world");
String v = new String(m.substring(0,2));

得到的結(jié)構(gòu)圖如下:

新字符數(shù)組在內(nèi)存中的布局

可以發(fā)現(xiàn),m,u,v內(nèi)部的字符數(shù)組并不是同一個掉伏,有興趣可以試驗一下缝呕。

常量池中字符串的產(chǎn)生

常量池中的字符串通常是通過字面量的方式產(chǎn)生的,就像上述m語句那樣斧散。 并且他們是在編譯的時候就準(zhǔn)備好了供常,類加載的時候,順便就在常量池生成鸡捐。

可以通過javap命令檢查一下class的字節(jié)碼栈暇,可以發(fā)現(xiàn)下面的高亮部分(以上面代碼為例):

javap -v StringTest

 Compiled from "StringTest.java"
 public class com.github.mccxj.StringTest extends java.lang.Object
   SourceFile: "StringTest.java"
   minor version: 0
   major version: 50
   Constant pool:
 const #1 = Method       #9.#28; //  java/lang/Object."<init>":()V
+ const #2 = String       #29;    //  hello,
+ const #3 = String       #30;    //  world
 ...
+ const #46 = Asciz       hello,;
+ const #47 = Asciz       world;
 ...

大家不知有沒有發(fā)現(xiàn),上面的圖中箍镜,u和v的字符數(shù)組沒有被常量池里邊的字符串引用到源祈。 原因就是這些字符串(字符數(shù)組)都是運行時生成的,而常量池里邊的字符串和字符數(shù)組是完整對應(yīng)上的(count等于數(shù)組長度)色迂。

即使是字符串的內(nèi)容是一樣的香缺,都不能保證是同一個字符串?dāng)?shù)組。例如下面的代碼:

String m = "hello,world";
String u = m + ".";
String v = "hello,world.";

u和v雖然是一樣內(nèi)容的字符串歇僧,但內(nèi)部的字符數(shù)組不是同一個图张。畫成圖的話就是這樣的:

不同字符數(shù)組在內(nèi)存中的布局

另外有一點,如果讓m聲明為final诈悍,你就會發(fā)現(xiàn)u和v變成是同一個對象祸轮。畫成圖的話就是這樣的:

u和v在內(nèi)存中的布局

這應(yīng)該怎么解釋的?這其實都是編譯器搞的鬼侥钳,因為m是final的适袜, u直接被編譯成”hello,world.”了,如果使用javap查看的話舷夺,會發(fā)現(xiàn)下面一段邏輯:

const #2 = String       #25;    //  hello,world
const #3 = String       #26;    //  hello,world.
...
public void test1()   throws java.lang.Exception;
  Code:
   Stack=1, Locals=4, Args_size=1
   0:   ldc     #2; //String hello,world
   2:   astore_1
   3:   ldc     #3; //String hello,world.
   5:   astore_2
   6:   ldc     #3; //String hello,world.
   8:   astore_3
   9:   return

那么苦酱,如何讓運行時產(chǎn)生的字符串放到常量池里邊呢? 可以借助String類的intern方法。 例如下面的用法:

String m = "hello,world";
String u = m.substring(0,2);
String v = u.intern();

上面我們已經(jīng)知道m(xù),n使用的是同一個字符數(shù)組冕房,但intern方法會到常量池里邊去尋找字符串”he”,如果找到的話躏啰,就直接返回該字符串, 否則就在常量池里邊創(chuàng)建一個并返回耙册,所以v使用的字符數(shù)組和m,n不是同一個给僵。畫成圖的話就是這樣的:

intern在內(nèi)存中的布局

字符串的內(nèi)存釋放問題

像字面量字符串,因為存放在常量池里邊详拙,被常量池引用著帝际,是沒法被GC的。例如下面的代碼:

String m = "hello,world";
String n = m.substring(0,2);

m = null;
n = null;

經(jīng)過上述的操作饶辙,畫成圖的話就是這樣的:

內(nèi)存釋放后的布局

而經(jīng)過上面的分析蹲诀,我們知道像substringsplit等方法得到的結(jié)果都是引用原字符數(shù)組的弃揽。 如果某字符串很大脯爪,而且不是在常量池里存在的则北,當(dāng)你采用substring等方法拿到一小部分新字符串之后,長期保存的話(例如用于緩存等)痕慢, 會造成原來的大字符數(shù)組意外無法被GC的問題尚揣。

關(guān)于這個問題,常見的解決辦法就是使用new String(String original)java.io.StreamTokenizer類掖举。并且在網(wǎng)上已經(jīng)有比較廣泛的討論快骗,大家可以去閱讀一下:

結(jié)論

  • 任何時候,比較字符串內(nèi)容都應(yīng)該使用equals方法塔次;
  • 修改字符串操作方篮,應(yīng)該使用StringBufferStringBuilder励负;
  • 可以使用intern方法讓運行時產(chǎn)生字符串的復(fù)用常量池中的字符串藕溅;
  • 字符串操作可能會復(fù)用原字符數(shù)組,在某些情況可能造成內(nèi)存泄露的問題继榆。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蜈垮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子裕照,更是在濱河造成了極大的恐慌攒发,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晋南,死亡現(xiàn)場離奇詭異惠猿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)负间,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進(jìn)店門偶妖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人政溃,你說我怎么就攤上這事趾访。” “怎么了董虱?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵扼鞋,是天一觀的道長。 經(jīng)常有香客問我愤诱,道長云头,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任淫半,我火速辦了婚禮溃槐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘科吭。我一直安慰自己昏滴,他們只是感情好猴鲫,可當(dāng)我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著谣殊,像睡著了一般变隔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蟹倾,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天,我揣著相機(jī)與錄音猖闪,去河邊找鬼鲜棠。 笑死,一個胖子當(dāng)著我的面吹牛培慌,可吹牛的內(nèi)容都是我干的豁陆。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼吵护,長吁一口氣:“原來是場噩夢啊……” “哼盒音!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起馅而,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤祥诽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后瓮恭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雄坪,經(jīng)...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年屯蹦,在試婚紗的時候發(fā)現(xiàn)自己被綠了维哈。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡登澜,死狀恐怖阔挠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情脑蠕,我是刑警寧澤购撼,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站谴仙,受9級特大地震影響份招,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狞甚,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一锁摔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧哼审,春花似錦谐腰、人聲如沸孕豹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽励背。三九已至,卻和暖如春砸西,著一層夾襖步出監(jiān)牢的瞬間叶眉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工芹枷, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留衅疙,地道東北人。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓鸳慈,卻偏偏與公主長得像饱溢,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子走芋,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法绩郎,類相關(guān)的語法,內(nèi)部類的語法翁逞,繼承相關(guān)的語法肋杖,異常的語法,線程的語...
    子非魚_t_閱讀 31,598評論 18 399
  • java筆記第一天 == 和 equals ==比較的比較的是兩個變量的值是否相等挖函,對于引用型變量表示的是兩個變量...
    jmychou閱讀 1,488評論 0 3
  • 前面我們總結(jié)了數(shù)組操作兽愤,這里我們將總結(jié)字符串相關(guān)的知識,除了總結(jié)String的API用法挪圾,同時我們還會總結(jié)一些相關(guān)...
    HCherisher閱讀 3,614評論 2 6
  • 很久以前浅萧, 我曾有過一個夢。 假造了一方不屬于自己的天地哲思, 我沉溺洼畅,我狂妄。 任性地丟掉自己棚赔,依附著其他帝簇。 終于,...
    隨風(fēng)的魚閱讀 154評論 3 2
  • “我的生活里芋浮,再也沒有你的影子;而你的身邊,也不再有我……” “我沒法笑著說不在乎壳快,說我根本沒愛過纸巷,我不是此等絕...
    遇見最好的灰閱讀 146評論 0 2