JAVA中的string

String 類

1
public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** 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;

    /** 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;

    ........
1)string類是final類,final類不能被繼承,並且他的成員方法都默認爲final方法在java中,被final修飾的類是不允許被繼承的,並且該類中的其他成員都是默認爲final方法

2)Stringl類是通過char數組來保存自字符串的。

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);
    }
    return ((beginIndex == 0) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    char buf[] = new char[count + otherLen];
    getChars(0, count, buf, 0);
    str.getChars(0, otherLen, buf, count);
    return new String(0, count + otherLen, buf);
}

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = count;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */
        int off = offset;   /* avoid getfield opcode */

        while (++i < len) {
        if (val[off + i] == oldChar) {
            break;
        }
        }
        if (i < len) {
        char buf[] = new char[len];
        for (int j = 0 ; j < i ; j++) {
            buf[j] = val[off+j];
        }
        while (i < len) {
            char c = val[off + i];
            buf[i] = (c == oldChar) ? newChar : c;
            i++;
        }
        return new String(0, len, buf);
        }
    }
    return this;
}
sub() contact() replace()操作都不是在原有的字符串上進行的,而是重新生成了一個新的字符串對象,進行這些操作後,原始的字符串並沒有被改變,。

String對象一旦被創建就是固定不變了,。對String對象的任何改變都不會影響到原對象,相關的任何操都會生成新的對象

二字符串常量池

當創建字符串常量的時候,jvm會首先檢查字符串常量池,如果該字符串已經存在常量池中,那麼就直接返回常量池的實例引用,如果字符串不存在常量池中,就會實例化該字符串並將其放到常量池中,由於String字符串的不可變性 我們可以十分的肯定常量池中一定不存在兩個相同的字符串

java中的常量池分爲兩種形態:靜態常量池和運行時常量池l
靜態常量池就是: *.class中的常量池,class文件中的常量池包括了字符串字面量,類,方法的信息,佔用了class文件的絕大部分空間,
運行時常量池: jvm虛擬機在完成類裝載操作後,將class文件中的常量載入到內存中,並保存在方法區中,經常說的常量池指的就是運行時常量池
String a=“abc”;
String b=“abc”;
a和b指向的是同一對象
String c=new String("abc");
       在這裏new 會產生一個新的對象“abc”這個對象是放在堆中的。 此時產生了兩個新的對象,一個放在棧中的c
堆中的“abc”

例子:
/**
 * 採用字面值的方式賦值
 */
public void test1(){
    String str1="aaa";
    String str2="aaa";
    System.out.println("===========test1============");
    System.out.println(str1==str2);//true 可以看出str1跟str2是指向同一個對象 
}


/**
 * 採用new關鍵字方式賦值
 */
public void test1(){
    String str3=new String("aaa");
    String str4=new String("aaa");
    System.out.println("===========test2============");
    System.out.println(str3==str4);//false 可以看出str3跟str4分別創建了一個新的對象 採用new關鍵字新建一個字符串對象時,JVM首先在字符串池中查找有沒有"aaa"這個字符串對象,如果有,則不在池中再去創建"aaa"這個對象了,直接在堆中創建一個"aaa"字符串對象,然後將堆中的這個"aaa"對象的地址返回賦給引用str3,這樣,str3就指向了堆中創建的這個"aaa"字符串對象;如果沒有,則首先在字符串池中創建一個"aaa"字符串對象,然後再在堆中創建一個"aaa"字符串對象,然後將堆中這個"aaa"字符串對象的地址返回賦給str3引用,這樣,str3指向了堆中創建的這個"aaa"字符串對象。當執行String str4=new String("aaa")時, 因爲採用new關鍵字創建對象時,每次new出來的都是一個新的對象,也即是說引用str3和str4指向的是兩個不同的對象,因此語句System.out.println(str3 == str4)輸出:false。
}
分析:
jvm現在字符串中查找有沒有aaa對象,如果有不在池中再創建新的,直接在堆中創建一個aaa字符串對象,然後將這個aaa對象的地址賦值給引用str3,如果沒有則首先在字符串池中創建一個aaa對象,再在重複上述操作,,最後將這個新創建的aaa對象的地址賦值引用str4, 由於在使用關鍵字new對象時,每次new的對象都是一個新的對象,每個對象都自己的唯一地址,所以str3==str4  false
public void test3(){
    String s0="helloworld";
    String s1="helloworld";
    String s2="hello"+"world";
    System.out.println("===========test3============");
    System.out.println(s0==s1); //true 可以看出s0跟s1是指向同一個對象 
    System.out.println(s0==s2); //true 可以看出s0跟s2是指向同一個對象 
}
s0和s1中的helloworld都是字符串常量,他們在編譯期就被確定了,hello world 同理也是如此,當一個字符串
進行字符串常量連接時,他自己也是自字符串常量,所以s2在編譯期被解析爲一個字符串常量


/**
 * 編譯期無法確定
 */
public void test4(){
    String s0="helloworld"; 
    String s1=new String("helloworld"); 
    String s2="hello" + new String("world"); 
    System.out.println("===========test4============");
    System.out.println( s0==s1 ); //false  
    System.out.println( s0==s2 ); //false 
    System.out.println( s1==s2 ); //false
}
分析:str3指向的是堆中的“abcdef ”,而"abcdef"是字符串池中的對象,所以結果爲false。JVM對String str="abc"對象放在常量池中是在編譯時做的,而String str3=str1+str2是在運行時刻才能知道的。new對象也是在運行時才做的。而這段代碼總共創建了5個對象,字符串池中兩個、堆中三個。+運算符會在堆中建立來兩個String對象,這兩個對象的值分別是"abc"和"def",也就是說從字符串池中複製這兩個值,然後在堆中創建兩個對象,然後再建立對象str3,然後將"abcdef"的堆地址賦給str3。

步驟: 
1)棧中開闢一塊中間存放引用str1,str1指向池中String常量"abc"。 
2)棧中開闢一塊中間存放引用str2,str2指向池中String常量"def"。 
3)棧中開闢一塊中間存放引用str3。
4)str1 + str2通過StringBuilder的最後一步toString()方法還原一個新的String對象"abcdef",因此堆中開闢一塊空間存放此對象。
5)引用str3指向堆中(str1 + str2)所還原的新String對象。 
6)str3指向的對象在堆中,而常量"abcdef"在池中,輸出爲false。

例子6:

複製代碼
/**
 * 編譯期優化
 */
public void test6(){
    String s0 = "a1"; 
    String s1 = "a" + 1; 
    System.out.println("===========test6============");
    System.out.println((s0 == s1)); //result = true  
    String s2 = "atrue"; 
    String s3= "a" + "true"; 
    System.out.println((s2 == s3)); //result = true  
    String s4 = "a3.4"; 
    String s5 = "a" + 3.4; 
    System.out.println((s4 == s5)); //result = true
}
複製代碼

執行上述代碼,結果爲:true、true、true

分析:在程序編譯期,JVM就將常量字符串的"+"連接優化爲連接後的值,拿"a" + 1來說,經編譯器優化後在class中就已經是a1。在編譯期其字符串常量的值就確定下來,故上面程序最終的結果都爲true。

例子7:

複製代碼
/**
 * 編譯期無法確定
 */
public void test7(){
    String s0 = "ab"; 
    String s1 = "b"; 
    String s2 = "a" + s1; 
    System.out.println("===========test7============");
    System.out.println((s0 == s2)); //result = false
}
複製代碼

執行上述代碼,結果爲:false

分析:JVM對於字符串引用,由於在字符串的"+"連接中,有字符串引用存在,而引用的值在程序編譯期是無法確定的,即"a" + s1無法被編譯器優化,只有在程序運行期來動態分配並將連接後的新地址賦給s2。所以上面程序的結果也就爲false。

例子8:

複製代碼
/**
 * 比較字符串常量的“+”和字符串引用的“+”的區別
 */
public void test8(){
    String test="javalanguagespecification";
    String str="java";
    String str1="language";
    String str2="specification";
    System.out.println("===========test8============");
    System.out.println(test == "java" + "language" + "specification");
    System.out.println(test == str + str1 + str2);
}
複製代碼

執行上述代碼,結果爲:true、false

分析:爲什麼出現上面的結果呢?這是因爲,字符串字面量拼接操作是在Java編譯器編譯期間就執行了,也就是說編譯器編譯時,直接把"java"、"language"和"specification"這三個字面量進行"+"操作得到一個"javalanguagespecification" 常量,並且直接將這個常量放入字符串池中,這樣做實際上是一種優化,將3個字面量合成一個,避免了創建多餘的字符串對象。而字符串引用的"+"運算是在Java運行期間執行的,即str + str2 + str3在程序執行期間纔會進行計算,它會在堆內存中重新創建一個拼接後的字符串對象。總結來說就是:字面量"+"拼接是在編譯期間進行的,拼接後的字符串存放在字符串池中;而字符串引用的"+"拼接運算實在運行時進行的,新創建的字符串存放在堆中。

對於直接相加字符串,效率很高,因爲在編譯器便確定了它的值,也就是說形如"I"+"love"+"java"; 的字符串相加,在編譯期間便被優化成了"Ilovejava"。對於間接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因爲在編譯器不會對引用變量進行優化。

例子9:

複製代碼
/**
 * 編譯期確定
 */
public void test9(){
    String s0 = "ab"; 
    final String s1 = "b"; 
    String s2 = "a" + s1;  
    System.out.println("===========test9============");
    System.out.println((s0 == s2)); //result = true
}
複製代碼

執行上述代碼,結果爲:true

分析:和例子7中唯一不同的是s1字符串加了final修飾,對於final修飾的變量,它在編譯時被解析爲常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的字節碼流中。所以此時的"a" + s1和"a" + "b"效果是一樣的。故上面程序的結果爲true。

例子10:

複製代碼
/**
 * 編譯期無法確定
 */
public void test10(){
    String s0 = "ab"; 
    final String s1 = getS1(); 
    String s2 = "a" + s1; 
    System.out.println("===========test10============");
    System.out.println((s0 == s2)); //result = false 
    
}

private static String getS1() {  
    return "b";   
}
複製代碼

執行上述代碼,結果爲:false

分析:這裏面雖然將s1用final修飾了,但是由於其賦值是通過方法調用返回的,那麼它的值只能在運行期間確定,因此s0和s2指向的不是同一個對象,故上面程序的結果爲false。

三、總結

1.String類初始化後是不可變的(immutable)

String使用private final char value[]來實現字符串的存儲,也就是說String對象創建之後,就不能再修改此對象中存儲的字符串內容,就是因爲如此,才說String類型是不可變的(immutable)。程序員不能對已有的不可變對象進行修改。我們自己也可以創建不可變對象,只要在接口中不提供修改數據的方法就可以。
然而,String類對象確實有編輯字符串的功能,比如replace()。這些編輯功能是通過創建一個新的對象來實現的,而不是對原有對象進行修改。比如:

s = s.replace("World", "Universe");

上面對s.replace()的調用將創建一個新的字符串"Hello Universe!",並返回該對象的引用。通過賦值,引用s將指向該新的字符串。如果沒有其他引用指向原有字符串"Hello World!",原字符串對象將被垃圾回收。


2.引用變量與對象

A aa;
這個語句聲明一個類A的引用變量aa[我們常常稱之爲句柄],而對象一般通過new創建。所以aa僅僅是一個引用變量,它不是對象。

3.創建字符串的方式

創建字符串的方式歸納起來有兩類:

(1)使用""引號創建字符串;

(2)使用new關鍵字創建字符串。

結合上面例子,總結如下:

(1)單獨使用""引號創建的字符串都是常量,編譯期就已經確定存儲到String Pool中;

(2)使用new String("")創建的對象會存儲到heap中,是運行期新創建的;

new創建字符串時首先查看池中是否有相同值的字符串,如果有,則拷貝一份到堆中,然後返回堆中的地址;如果池中沒有,則在堆中創建一份,然後返回堆中的地址(注意,此時不需要從堆中複製到池中,否則,將使得堆中的字符串永遠是池中的子集,導致浪費池的空間)!

(3)使用只包含常量的字符串連接符如"aa" + "aa"創建的也是常量,編譯期就能確定,已經確定存儲到String Pool中;

(4)使用包含變量的字符串連接符如"aa" + s1創建的對象是運行期才創建的,存儲在heap中;

4.使用String不一定創建對象

在執行到雙引號包含字符串的語句時,如String a = "123",JVM會先到常量池裏查找,如果有的話返回常量池裏的這個實例的引用,否則的話創建一個新實例並置入常量池裏。所以,當我們在使用諸如String str = "abc";的格式定義對象時,總是想當然地認爲,創建了String類的對象str。擔心陷阱!對象可能並沒有被創建!而可能只是指向一個先前已經創建的對象。只有通過new()方法才能保證每次都創建一個新的對象。

5.使用new String,一定創建對象

在執行String a = new String("123")的時候,首先走常量池的路線取到一個實例的引用,然後在堆上創建一個新的String實例,走以下構造函數給value屬性賦值,然後把實例引用賦值給a:

複製代碼
public String(String original) {
    int size = original.count;
    char[] originalValue = original.value;
    char[] v;
      if (originalValue.length > size) {
         // The array representing the String is bigger than the new
         // String itself.  Perhaps this constructor is being called
         // in order to trim the baggage, so make a copy of the array.
            int off = original.offset;
            v = Arrays.copyOfRange(originalValue, off, off+size);
     } else {
         // The array representing the String is the same
         // size as the String, so no point in making a copy.
        v = originalValue;
     }
    this.offset = 0;
    this.count = size;
    this.value = v;
    }
複製代碼

從中我們可以看到,雖然是新創建了一個String的實例,但是value是等於常量池中的實例的value,即是說沒有new一個新的字符數組來存放"123"。

6.關於String.intern()

intern方法使用:一個初始爲空的字符串池,它由類String獨自維護。當調用 intern方法時,如果池已經包含一個等於此String對象的字符串(用equals(oject)方法確定),則返回池中的字符串。否則,將此String對象添加到池中,並返回此String對象的引用。

它遵循以下規則:對於任意兩個字符串 s 和 t,當且僅當 s.equals(t) 爲 true 時,s.intern() == t.intern() 才爲 true。

String.intern(); 
再補充介紹一點:存在於.class文件中的常量池,在運行期間被jvm裝載,並且可以擴充。String的intern()方法就是擴充常量池的一個方法;當一個String實例str調用intern()方法時,java查找常量池中是否有相同unicode的字符串常量,如果有,則返回其引用,如果沒有,則在常量池中增加一個unicode等於str的字符串並返回它的引用。

複製代碼
/**
 * 關於String.intern()
 */
public void test11(){
    String s0 = "kvill"; 
    String s1 = new String("kvill"); 
    String s2 = new String("kvill"); 
    System.out.println("===========test11============");
    System.out.println( s0 == s1 ); //false
    System.out.println( "**********" ); 
    s1.intern(); //雖然執行了s1.intern(),但它的返回值沒有賦給s1
    s2 = s2.intern(); //把常量池中"kvill"的引用賦給s2 
    System.out.println( s0 == s1); //flase
    System.out.println( s0 == s1.intern() ); //true//說明s1.intern()返回的是常量池中"kvill"的引用
    System.out.println( s0 == s2 ); //true
}
複製代碼

運行結果:false、false、true、true。

7.關於equals和==

(1)對於==,如果作用於基本數據類型的變量(byte,short,char,int,long,float,double,boolean ),則直接比較其存儲的"值"是否相等;如果作用於引用類型的變量(String),則比較的是所指向的對象的地址(即是否指向同一個對象)。

(2)equals方法是基類Object中的方法,因此對於所有的繼承於Object的類都會有該方法。在Object類中,equals方法是用來比較兩個對象的引用是否相等,即是否指向同一個對象。

(3)對於equals方法,注意:equals方法不能作用於基本數據類型的變量。如果沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;而String類對equals方法進行了重寫,用來比較指向的字符串對象所存儲的字符串是否相等。其他的一些類諸如Double,Date,Integer等,都對equals方法進行了重寫用來比較指向的對象所存儲的內容是否相等。

複製代碼
/**
 * 關於equals和==
 */
public void test12(){
    String s1="hello";
    String s2="hello";
    String s3=new String("hello");
    System.out.println("===========test12============");
    System.out.println( s1 == s2); //true,表示s1和s2指向同一對象,它們都指向常量池中的"hello"對象
    //flase,表示s1和s3的地址不同,即它們分別指向的是不同的對象,s1指向常量池中的地址,s3指向堆中的地址
    System.out.println( s1 == s3); 
    System.out.println( s1.equals(s3)); //true,表示s1和s3所指向對象的內容相同
}
複製代碼

8.String相關的+:

String中的 + 常用於字符串的連接。看下面一個簡單的例子:

複製代碼
/**
 * String相關的+
 */
public void test13(){
    String a = "aa";
    String b = "bb";
    String c = "xx" + "yy " + a + "zz" + "mm" + b;
    System.out.println("===========test13============");
    System.out.println(c);
}
複製代碼

編譯運行後,主要字節碼部分如下:

複製代碼
public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    LDC "aa"
    ASTORE 1
   L1
    LINENUMBER 6 L1
    LDC "bb"
    ASTORE 2
   L2
    LINENUMBER 7 L2
    NEW java/lang/StringBuilder
    DUP
    LDC "xxyy "
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ALOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "zz"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "mm"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 2
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 3
   L3
    LINENUMBER 8 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 3
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 9 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE a Ljava/lang/String; L1 L5 1
    LOCALVARIABLE b Ljava/lang/String; L2 L5 2
    LOCALVARIABLE c Ljava/lang/String; L3 L5 3
    MAXSTACK = 3
    MAXLOCALS = 4
}
複製代碼

顯然,通過字節碼我們可以得出如下幾點結論:
(1).String中使用 + 字符串連接符進行字符串連接時,連接操作最開始時如果都是字符串常量,編譯後將儘可能多的直接將字符串常量連接起來,形成新的字符串常量參與後續連接(通過反編譯工具jd-gui也可以方便的直接看出);

(2).接下來的字符串連接是從左向右依次進行,對於不同的字符串,首先以最左邊的字符串爲參數創建StringBuilder對象,然後依次對右邊進行append操作,最後將StringBuilder對象通過toString()方法轉換成String對象(注意:中間的多個字符串常量不會自動拼接)。

也就是說String c = "xx" + "yy " + a + "zz" + "mm" + b; 實質上的實現過程是: String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();

由此得出結論:當使用+進行多個字符串連接時,實際上是產生了一個StringBuilder對象和一個String對象。

9.String的不可變性導致字符串變量使用+號的代價:

String s = "a" + "b" + "c"; 
String s1  =  "a"; 
String s2  =  "b"; 
String s3  =  "c"; 
String s4  =  s1  +  s2  +  s3;

分析:變量s的創建等價於 String s = "abc"; 由上面例子可知編譯器進行了優化,這裏只創建了一個對象。由上面的例子也可以知道s4不能在編譯期進行優化,其對象創建相當於:

StringBuilder temp = new StringBuilder();   
temp.append(a).append(b).append(c);   
String s = temp.toString();

由上面的分析結果,可就不難推斷出String 採用連接運算符(+)效率低下原因分析,形如這樣的代碼:

複製代碼
public class Test {
    public static void main(String args[]) {
        String s = null;
        for(int i = 0; i < 100; i++) {
            s += "a";
        }
    }
}
複製代碼

每做一次 + 就產生個StringBuilder對象,然後append後就扔掉。下次循環再到達時重新產生個StringBuilder對象,然後 append 字符串,如此循環直至結束。 如果我們直接採用 StringBuilder 對象進行 append 的話,我們可以節省 N - 1 次創建和銷燬對象的時間。所以對於在循環中要進行字符串連接的應用,一般都是用StringBuffer或StringBulider對象來進行append操作。

10.String、StringBuffer、StringBuilder的區別

(1)可變與不可變:String是不可變字符串對象,StringBuilder和StringBuffer是可變字符串對象(其內部的字符數組長度可變)。

(2)是否多線程安全:String中的對象是不可變的,也就可以理解爲常量,顯然線程安全。StringBuffer 與 StringBuilder 中的方法和功能完全是等價的,只是StringBuffer 中的方法大都採用了synchronized 關鍵字進行修飾,因此是線程安全的,而 StringBuilder 沒有這個修飾,可以被認爲是非線程安全的。

(3)String、StringBuilder、StringBuffer三者的執行效率:
StringBuilder > StringBuffer > String 當然這個是相對的,不一定在所有情況下都是這樣。比如String str = "hello"+ "world"的效率就比 StringBuilder st  = new StringBuilder().append("hello").append("world")要高。因此,這三個類是各有利弊,應當根據不同的情況來進行選擇使用:
當字符串相加操作或者改動較少的情況下,建議使用 String str="hello"這種形式;
當字符串相加操作較多的情況下,建議使用StringBuilder,如果採用了多線程,則使用StringBuffer。

11.String中的final用法和理解

final StringBuffer a = new StringBuffer("111");
final StringBuffer b = new StringBuffer("222");
a=b;//此句編譯不通過

final StringBuffer a = new StringBuffer("111");
a.append("222");//編譯通過

可見,final只對引用的"值"(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象的變化,final是不負責的。

13.字符串池的優缺點:
字符串池的優點就是避免了相同內容的字符串的創建,節省了內存,省去了創建相同字符串的時間,同時提升了性能;另一方面,字符串池的缺點就是犧牲了JVM在常量池中遍歷對象所需要的時間,不過其時間成本相比而言比較低。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章