string stringBufer

面試中常常會遇到這樣的問題:1、你瞭解String類嗎?2、String,StringBuilder和StringBuffer適合在什麼樣的場景下使用?

1、String類

首先看一下String的源碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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修飾的。在之前的JVM實現版本中,被final修飾的方法會被轉爲內嵌調用以便提升效率。但是從JDK5/6之後,就拋棄了這種方式。因此現在不需要考慮通過final修飾來提高方法的調用效率。一般情況下,如果想要某個方法不被覆蓋或者重載的情況下使用final修飾。

2)String採用immutable 設計模式,因此對象是最終的,不可被改變(在JDK的標準下是這樣,當繞過JDK標準時,採用反射的方式就可以改變)。

  • String s1 = "abcd";  
  • String s2 = "abcd";  
  • s1和s2是兩個不同的對象

    Q:爲什麼將其設爲不可變的,有什麼好處?(瞭解)

    a.字符串常量池的需要

    字符串常量池(String pool, String intern pool, String保留池) 是Java堆內存中一個特殊的存儲區域, 當創建一個String對象時,假如此字符串值已經存在於常量池中,則不會創建一個新的對象,而是引用已經存在的對象。

    如下面的代碼所示,將會在堆內存中只創建一個實際String對象.

    1. String s1 = "abcd";  
    2. String s2 = "abcd";  
    假若字符串對象允許改變,那麼將會導致各種邏輯錯誤,比如改變一個對象會影響到另一個獨立對象. 嚴格來說,這種常量池的思想,是一種優化手段.

    b.允許String對象緩存HashCode
    Java中String對象的哈希碼被頻繁地使用, 比如在hashMap 等容器中。

    字符串不變性保證了hash碼的唯一性,因此可以放心地進行緩存.這也是一種性能優化手段,意味着不必每次都去計算新的哈希碼. 在String類的定義中有如下代碼:

    1. private int hash;//用來緩存HashCode  
    c.安全性
    String被許多的Java類(庫)用來當做參數,例如 網絡連接地址URL,文件路徑path,還有反射機制所需要的String參數等, 假若String不是固定不變的,將會引起各種安全隱患。

    假如有如下的代碼:

    1. boolean connect(string s){  
    2.     if (!isSecure(s)) {   
    3. throw new SecurityException();   
    4. }  
    5.     // 如果在其他地方可以修改String,那麼此處就會引起各種預料不到的問題/錯誤   
    6.     causeProblem(s);  
    7. }  

    3)上面列舉出了String類中所有的成員屬性,從上面可以看出String類其實是通過char數組來保存字符串的。

      下面再繼續看String類的一些方法實現:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    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操、concat還是replace操作都不是在原有的字符串上進行的,而是重新生成了一個新的字符串對象。也就是說進行這些操作後,最原始的字符串並沒有被改變。

      在這裏要永遠記住一點:

      “對String對象的任何改變都不影響到原對象,相關的任何change操作都會生成新的對象”。

      在瞭解了於String類基礎的知識後,下面來看一些在平常使用中容易忽略和混淆的地方。

    String str="hello world"和String str=new String("hello world")的區別

    想必大家對上面2個語句都不陌生,在平時寫代碼的過程中也經常遇到,那麼它們到底有什麼區別和聯繫呢?下面先看幾個例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Main {
             
        public static void main(String[] args) {
            String str1 = "hello world";
            String str2 = new String("hello world");
            String str3 = "hello world";
            String str4 = new String("hello world");
             
            System.out.println(str1==str2);
            System.out.println(str1==str3);
            System.out.println(str2==str4);
        }
    }
    輸出的結果是:

    false

    true

    false

    爲什麼會出現這樣的結果?下面解釋一下原因:

      在前面一篇講解關於JVM內存機制的一篇博文中提到 ,在class文件中有一部分 來存儲編譯期間生成的 字面常量以及符號引用,這部分叫做class文件常量池,在運行期間對應着方法區的運行時常量池。

      因此在上述代碼中,String str1 = "hello world";和String str3 = "hello world"; 都在編譯期間生成了 字面常量和符號引用,運行期間字面常量"hello world"被存儲在運行時常量池(當然只保存了一份)。通過這種方式來將String對象跟引用綁定的話,JVM執行引擎會先在運行時常量池查找是否存在相同的字面常量,如果存在,則直接將引用指向已經存在的字面常量;否則在運行時常量池開闢一個空間來存儲該字面常量,並將引用指向該字面常量。

    衆所周知,通過new關鍵字來生成對象是在堆區進行的,而在堆區進行對象生成的過程是不會去檢測該對象是否已經存在的。因此通過new來創建對象,創建出的一定是不同的對象,即使字符串的內容是相同的。

    String、StringBuffer以及StringBuilder的區別

    既然在Java中已經存在了String類,那爲什麼還需要StringBuilder和StringBuffer類呢?

      那麼看下面這段代碼:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Main {
             
        public static void main(String[] args) {
            String string = "";
            for(int i=0;i<10000;i++){
                string += "hello";
            }
        }
    }

      這句 string += "hello";的過程相當於將原有的string變量指向的對象內容取出與"hello"作字符串相加操作再存進另一個新的String對象當中,再讓string變量指向新生成的對象。如果大家還有疑問可以反編譯其字節碼文件便清楚了


    從這段反編譯出的字節碼文件可以很清楚地看出:從第8行開始到第35行是整個循環的執行過程,並且每次循環會new出一個StringBuilder對象,然後進行append操作,最後通過toString方法返回String對象。也就是說這個循環執行完畢new出了10000個對象,試想一下,如果這些對象沒有被回收,會造成多大的內存資源浪費。從上面還可以看出:string+="hello"的操作事實上會自動被JVM優化成:

      StringBuilder str = new StringBuilder(string);

      str.append("hello");

      str.toString();

      再看下面這段代碼:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Main {
             
        public static void main(String[] args) {
            StringBuilder stringBuilder = new StringBuilder();
            for(int i=0;i<10000;i++){
                stringBuilder.append("hello");
            }
        }
    }
    反編譯字節碼文件得到:


    從這裏可以明顯看出,這段代碼的for循環式從13行開始到27行結束,並且new操作只進行了一次,也就是說只生成了一個對象,append操作是在原有對象的基礎上進行的。因此在循環了10000次之後,這段代碼所佔的資源要比上面小得多。

      那麼有人會問既然有了StringBuilder類,爲什麼還需要StringBuffer類?查看源代碼便一目瞭然,事實上,StringBuilder和StringBuffer類擁有的成員屬性以及成員方法基本相同,區別是StringBuffer類的成員方法前面多了一個關鍵字:synchronized,不用多說,這個關鍵字是在多線程訪問時起到安全保護作用的,也就是說StringBuffer是線程安全的。

      下面摘了2段代碼分別來自StringBuffer和StringBuilder,insert方法的具體實現:

      StringBuilder的insert方法

    1
    2
    3
    4
    5
    6
    public StringBuilder insert(int index, char str[], int offset,
                                  int len)
      {
          super.insert(index, str, offset, len);
      return this;
      }

      StringBuffer的insert方法:

    1
    2
    3
    4
    5
    6
    public synchronized StringBuffer insert(int index, char str[], int offset,
                                                int len)
        {
            super.insert(index, str, offset, len);
            return this;
        }
    不同場景下的三個類的性能測試

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    public class Main {
        private static int time = 50000;
        public static void main(String[] args) {
            testString();
            testStringBuffer();
            testStringBuilder();
            test1String();
            test2String();
        }
         
         
        public static void testString () {
            String s="";
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++){
                s += "java";
            }
            long over = System.currentTimeMillis();
            System.out.println("操作"+s.getClass().getName()+"類型使用的時間爲:"+(over-begin)+"毫秒");
        }
         
        public static void testStringBuffer () {
            StringBuffer sb = new StringBuffer();
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++){
                sb.append("java");
            }
            long over = System.currentTimeMillis();
            System.out.println("操作"+sb.getClass().getName()+"類型使用的時間爲:"+(over-begin)+"毫秒");
        }
         
        public static void testStringBuilder () {
            StringBuilder sb = new StringBuilder();
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++){
                sb.append("java");
            }
            long over = System.currentTimeMillis();
            System.out.println("操作"+sb.getClass().getName()+"類型使用的時間爲:"+(over-begin)+"毫秒");
        }
         
        public static void test1String () {
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++){
                String s = "I"+"love"+"java";
            }
            long over = System.currentTimeMillis();
            System.out.println("字符串直接相加操作:"+(over-begin)+"毫秒");
        }
         
        public static void test2String () {
            String s1 ="I";
            String s2 = "love";
            String s3 = "java";
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++){
                String s = s1+s2+s3;
            }
            long over = System.currentTimeMillis();
            System.out.println("字符串間接相加操作:"+(over-begin)+"毫秒");
        }
         
    }

    測試結果(win7,Eclipse,JDK6):


    上面提到string+="hello"的操作事實上會自動被JVM優化,看下面這段代碼:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class Main {
        private static int time = 50000;
        public static void main(String[] args) {
            testString();
            testOptimalString();
        }
         
         
        public static void testString () {
            String s="";
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++){
                s += "java";
            }
            long over = System.currentTimeMillis();
            System.out.println("操作"+s.getClass().getName()+"類型使用的時間爲:"+(over-begin)+"毫秒");
        }
         
        public static void testOptimalString () {
            String s="";
            long begin = System.currentTimeMillis();
            for(int i=0; i<time; i++){
                StringBuilder sb = new StringBuilder(s);
                sb.append("java");
                s=sb.toString();
            }
            long over = System.currentTimeMillis();
            System.out.println("模擬JVM優化操作的時間爲:"+(over-begin)+"毫秒");
        }
         
    }
        

      執行結果:


    得到驗證。

      下面對上面的執行結果進行一般性的解釋:

      1)對於直接相加字符串,效率很高,因爲在編譯器便確定了它的值,也就是說形如"I"+"love"+"java"; 的字符串相加,在編譯期間便被優化成了"Ilovejava"。這個可以用javap -c命令反編譯生成的class文件進行驗證。

      對於間接相加(即包含字符串引用),形如s1+s2+s3; 效率要比直接相加低,因爲在編譯器不會對引用變量進行優化。

      2)String、StringBuilder、StringBuffer三者的執行效率:

      StringBuilder > StringBuffer > String

      當然這個是相對的,不一定在所有情況下都是這樣。

      比如String str = "hello"+ "world"的效率就比 StringBuilder st  = new StringBuilder().append("hello").append("world")要高。

      因此,這三個類是各有利弊,應當根據不同的情況來進行選擇使用:

      當字符串相加操作或者改動較少的情況下,建議使用 String str="hello"這種形式;

      當字符串相加操作較多的情況下,建議使用StringBuilder,如果採用了多線程,則使用StringBuffer。

    說了這麼多,這三個類在什麼場景下使用呢?

    3、三個類是使用場景

    不需要頻繁的拼接字符串的時候使用String,相反需要經常拼接字符串的時候使用StringBuilder。StringBuilder與StringBuffer相比,兩個類似,不同的是StringBuilder是非線程安全的,適合在單線程的情況下使用。多線程情況也可以使用,不過需要手動加同步。StrngBuffer是線程安全的,適合在多線程的情況下使用。StringBuilder的效率要StringBuffer高

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