天天看點

Java的string類常量池及不可變性

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"; 

示意圖如下所示:

Java的string類常量池及不可變性

        假若字元串對象允許改變,那麼将會導緻各種邏輯錯誤,比如改變一個對象會影響到另一個獨立對象. 嚴格來說,這種常量池的思想,是一種優化手段.

請思考: 假若代碼如下所示,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&lt;string&gt;, 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&lt;string&gt;, 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

     * &lt;a href="{@docroot}/../platform/serialization/spec/output.html"&gt;

     * object serialization

specification, section 6.2, "stream elements"&lt;/a&gt;

     */

    // 聲明序列化時要包含的域,僅僅聲明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”; 這句代碼之後,真正的記憶體布局應該是這樣的:

Java的string類常量池及不可變性

        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 &lt; len)

                if (val[i]

== oldchar) {

                    break;

                }

            }

            if (i &lt; len)

                char buf[]

= new char[len];

                for (int j =

0; j &lt; i; j++)

                    buf[j]

= val[j];

                while (i &lt; 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