Java基礎1-String詳解

概覽

1. 類聲明

String 被聲明爲 final,所以它不可被繼承。html

在 Java 8 及以前,內部使用 char 數組存儲數據。java

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
}
複製代碼

在 Java 9 及以後,String 類的實現改用 byte 數組存儲字符串,同時使用 coder來標識使用了哪一種字符集編碼。c++

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final byte[] value;

    /** The identifier of the encoding used to encode the bytes in {@code value}. */
    private final byte coder;
}
複製代碼

2. 構造函數

  1. 空參構造正則表達式

    /** * final聲明的 value數組不能修改它的引用,因此在構造函數中必定要初始化value屬性 */
    public String() {
            this.value = "".value;
        }
    複製代碼
  2. 用一個String來構造api

    /** * 除非你明確須要 這個original字符串的 副本 */
        public String(String original) {
            this.value = original.value;
            this.hash = original.hash;
        }
    複製代碼
  3. 用char數組來構造數組

    public String(char value[], int offset, int count) {
            if (offset < 0) {
                throw new StringIndexOutOfBoundsException(offset);
            }
            if (count <= 0) {
                if (count < 0) {
                    throw new StringIndexOutOfBoundsException(count);
                }
                if (offset <= value.length) {
                    this.value = "".value;
                    return;
                }
            }
            // Note: offset or count might be near -1>>>1.
            if (offset > value.length - count) {
                throw new StringIndexOutOfBoundsException(offset + count);
            }
            this.value = Arrays.copyOfRange(value, offset, offset+count);
        }
    複製代碼
  4. 用byte[]來構造緩存

    /** * 構造一個由byte[]生產的字符串,使用系統默認字符集編碼 * 新數組的長度 不必定等於 數組的length * 若是默認字符集編碼不可用時,此構造器無效。 */
        public String(byte bytes[], int offset, int length) {
            checkBounds(bytes, offset, length);
            this.value = StringCoding.decode(bytes, offset, length);
        }
    複製代碼
  5. 用 Unicode編碼的int[]來構造安全

    /** * 使用 Unicode編碼的int數組 初始化字符串 * 入參數組修改不影響新建立的String * @since 1.5 */
        public String(int[] codePoints, int offset, int count) {
            if (offset < 0) {
                throw new StringIndexOutOfBoundsException(offset);
            }
            if (count <= 0) {
                if (count < 0) {
                    throw new StringIndexOutOfBoundsException(count);
                }
                //count = 0
                if (offset <= codePoints.length) {
                    this.value = "".value;
                    return;
                }
            }
            // Note: offset or count might be near -1>>>1.
            if (offset > codePoints.length - count) {
                throw new StringIndexOutOfBoundsException(offset + count);
            }
    
            final int end = offset + count;
    
            // Pass 1: Compute precise size of char[]
            int n = count;
            for (int i = offset; i < end; i++) {
                int c = codePoints[i];
                // 從 U+0000 至 U+FFFF 之間的字符集有時候被稱爲基本多語言面
                // 可使用單個char來表示這樣的代碼點
                if (Character.isBmpCodePoint(c))
                    continue;
                // 確認c 是否是
                else if (Character.isValidCodePoint(c))
                    n++;
                else throw new IllegalArgumentException(Integer.toString(c));
            }
    
            // Pass 2: Allocate and fill in char[]
            // 獲得能夠轉成有效字符的 個數
            final char[] v = new char[n];
    
            for (int i = offset, j = 0; i < end; i++, j++) {
                int c = codePoints[i];
                if (Character.isBmpCodePoint(c))
                    v[j] = (char)c;
                else
                    Character.toSurrogates(c, v, j++);
            }
    
            this.value = v;
        }
    }
    複製代碼
  6. 用變長字符串StringBuffer,StringBuilder來構造網絡

    public String(StringBuffer buffer) {
    	synchronized(buffer) {
    		this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    	}
    }
    
    public String(StringBuilder builder) {
    	this.value = Arrays.copyOf(builder.getValue(), builder.length());
    }
    複製代碼

3. 經常使用api

方法列表:數據結構

boolean isEmpty() //當且僅當 length() 爲 0 時返回 true int length() //返回此字符串的長度 boolean contains(CharSequence s) //當且僅當此字符串包含指定的 char 值序列時,返回 true char charAt(int index) //返回指定索引處的 char 值 String concat(String str) //將指定字符串鏈接到此字符串的結尾 int indexOf(int ch) //返回指定字符在此字符串中第一次出現處的索引 int lastIndexOf(int ch) //返回指定字符在此字符串中最後一次出現處的索引  String substring(int beginIndex, int endIndex) //返回一個新字符串,它是此字符串的一個子字符串 CharSequence subSequence(int beginIndex, int endIndex) //返回一個新的字符序列,它是此序列的一個子序列 int compareTo(String anotherString) //按字典順序比較兩個字符串 int compareToIgnoreCase(String str) //按字典順序比較兩個字符串,不考慮大小寫 boolean equalsIgnoreCase(String anotherString) //將此 String 與另外一個 String 比較,不考慮大小寫 static String valueOf(double d) static String valueOf(boolean b) byte[] getBytes(Charset charset) //使用給定的 charset 將此 String 編碼到 byte 序列,並將結果存儲到新的 byte 數組 byte[] getBytes(String charsetName) //使用指定的字符集將此 String 編碼爲 byte 序列,並將結果存儲到一個新的 byte 數組中 String toLowerCase(Locale locale) //使用給定 Locale 的規則將此 String 中的全部字符都轉換爲小寫  String toUpperCase(Locale locale) boolean matches(String regex) //告知此字符串是否匹配給定的正則表達式 String[] split(String regex, int limit) //根據匹配給定的正則表達式來拆分此字符串 boolean startsWith(String prefix, int toffset) //測試此字符串從指定索引開始的子字符串是否以指定前綴開始 boolean endsWith(String suffix) static String copyValueOf(char[] data)//返回指定數組中表示該字符序列的 char[] toCharArray() //將此字符串轉換爲一個新的字符數組 String replace(char oldChar, char newChar) //返回一個新的字符串,它是經過用 newChar 替換此字符串中出現的全部 oldChar 獲得的 String replaceAll(String regex, String replacement) //使用給定的 replacement 替換此字符串全部匹配給定的正則表達式的子字符串 String intern() //返回字符串對象的規範化表示形式,字符串pool中的存在返回,不存在存入pool並返回 String trim()//返回字符串的副本,忽略前導空白和尾部空白 static String format(Locale l, String format, Object... args) //使用指定的語言環境、格式字符串和參數返回一個格式化字符串  複製代碼

4. 不可修改的特色

爲什麼不可修改

如下兩點保證String的不可修改特色

  1. value 被聲明爲 final,即value引用的地址不可修改。
  2. String類沒有暴露修改value引用內容的方法。
不可修改的優勢

從內存,同步和數據結構角度分析:

  1. Requirement of String Pool:字符串池(String intern pool)是方法區域中的特殊存儲區域。 建立字符串而且池中已存在該字符串時,將返回現有字符串的引用,而不是建立新對象。若是字符串可變,這將毫無心義。

  2. Caching Hashcode:hashcode在java中被頻繁的使用,在String類中存在屬性 private int hash;//this is used to cache hash code.

  3. Facilitating the Use of Other Objects:確保第三方使用。舉一個例子:

    //假設String.class 有屬性 value;
    //set的本意是保證元素不重複出現,若是String是可變的,則會破壞這個規則
    HashSet<String> set = new HashSet<String>();
    set.add(new String("a"));
    set.add(new String("b"));
    set.add(new String("c"));
     
    for(String a: set)
    	a.value = "a";
    複製代碼
  4. Security:String被普遍用做許多java類的參數,例如 網絡鏈接,打開文件等。字符串不是不可變的,鏈接或文件將被更改,這可能會致使嚴重的安全威脅。 該方法認爲它鏈接到一臺機器,但事實並不是如此。 可變字符串也可能在Reflection中引發安全問題,由於參數是字符串。例子:

    boolean connect(string s){
        if (!isSecure(s)) { 
    throw new SecurityException(); 
    }
        //here will cause problem, if s is changed before this by using other references. 
        causeProblem(s);
    }
    複製代碼
  5. Immutable objects are naturally thread-safe:因爲沒法更改不可變對象,所以能夠在多個線程之間自由共享它們。 這消除了進行同步的要求。

總之,出於效率和安全緣由,String被設計爲不可變的。 這也是在通常狀況下在一些狀況下優選不可變類的緣由。

5. 字符串pool

什麼是池

在 JAVA 語言中有8中基本類型和一種比較特殊的類型String。這些類型爲了使他們在運行過程當中速度更快,更節省內存,都提供了一種常量池的概念。常量池就相似一個JAVA系統級別提供的緩存。8種基本類型的常量池都是系統協調的,String類型的常量池比較特殊。它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的String對象會直接存儲在常量池中
  • 若是不是用雙引號聲明的String對象,可使用String提供的intern方法。intern 方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中
  • 在 jdk6 及之前的版本中,字符串的常量池是放在堆的 Perm 區的(Perm 區是一個類靜態的區域,主要存儲一些加載類的信息,常量池,方法片斷等內容,默認大小隻有4m),一旦常量池中大量使用 intern 是會直接產生java.lang.OutOfMemoryError: PermGen space錯誤的。
  • 在jdk7中,字符串常量池已經從 Perm 區移到正常的 Java Heap 區域。
String#intern方法

它的大致實現結構就是: JAVA 使用 jni 調用c++實現的StringTableintern方法, StringTableintern方法跟Java中的HashMap的實現是差很少的, 只是不能自動擴容。默認大小是1009

注意點:

  1. String的String Pool是一個固定大小的Hashtable,默認值大小長度是1009
  2. 若是放進String Pool的String很是多,就會形成Hash衝突嚴重,從而致使鏈表會很長,致使調用String.intern時性能會大幅降低(由於要一個一個找)
  3. 在 jdk6中StringTable的長度是固定 = 1009,因此若是常量池中的字符串過多就會致使效率降低很快。在jdk7中,StringTable的長度能夠經過一個參數指定:-XX:StringTableSize=99991
實例思考
// JDK6 中執行: false false
// JDK7 中執行: false true
public static void main(String[] args) {
    // 聲明的字符創變量 -> 堆
    String s = new String("1");
    s.intern();
    // 聲明的字符創常量 -> 堆的 Perm 區
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);
}

// JDK6 中執行: false false
// JDK7 中執行: false false
public static void main(String[] args) {
    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);
}
複製代碼
  • jdk6內存分析(注:圖中綠色線條表明 string 對象的內容指向。 黑色線條表明地址指向)
    jdk6內存分析
    • String s = new String("1");Perm中的常量池 生成"1",堆中生成 變量s內容="1"
    • s2 -> 常量池中的"1"
    • String s3 = new String("1") + new String("1");Perm中的常量池 生成"1",堆中生成 兩個匿名string 內容="1" 和 變量 s3
    • s.intern();將"11"寫入常量池
  • jdk7內存分析-1

jdk7內存分析-1

  • 在第一段代碼中,先看 s3和s4字符串。String s3 = new String("1") + new String("1");,這句代碼中如今生成了2最終個對象,是字符串常量池中的「1」 和 JAVA Heap 中的 s3引用指向的對象。中間還有2個匿名的new String("1")咱們不去討論它們。此時s3引用對象內容是」11」,但此時常量池中是沒有 「11」對象的。
  • 接下來s3.intern();這一句代碼,是將 s3中的「11」字符串放入 String 常量池中,由於此時常量池中不存在「11」字符串,所以常規作法是跟 jdk6 圖中表示的那樣,在常量池中生成一個 「11」 的對象,關鍵點是 jdk7 中常量池不在 Perm 區域了,這塊作了調整。常量池中不須要再存儲一份對象了,能夠直接存儲堆中的引用。這份引用指向 s3 引用的對象。 也就是說引用地址是相同的。
  • 最後String s4 = "11"; 這句代碼中」11」是顯示聲明的,所以會直接去常量池中建立,建立的時候發現已經有這個對象了,此時也就是指向 s3 引用對象的一個引用。因此 s4 引用就指向和 s3 同樣了。所以最後的比較 s3 == s4 是 true。
  • 再看 s 和 s2 對象。 String s = new String("1"); 第一句代碼,生成了2個對象。常量池中的「1」 和 JAVA Heap 中的字符串對象。s.intern(); 這一句是 s 對象去常量池中尋找後發現 「1」 已經在常量池裏了。
  • 接下來String s2 = "1"; 這句代碼是生成一個 s2的引用指向常量池中的「1」對象。 結果就是 s 和 s2 的引用地址明顯不一樣。圖中畫的很清晰。
  • jdk7內存分析-2

jdk7內存分析-2

  • 來看第二段代碼,從上邊第二幅圖中觀察。第一段代碼和第二段代碼的改變就是 s3.intern(); 的順序是放在String s4 = "11";後了。這樣,首先執行String s4 = "11";聲明 s4 的時候常量池中是不存在「11」對象的,執行完畢後,「11「對象是 s4 聲明產生的新對象。而後再執行s3.intern();時,常量池中「11」對象已經存在了,所以 s3 和 s4 的引用是不一樣的。
  • 第二段代碼中的 s 和 s2 代碼中,s.intern();,這一句日後放也不會有什麼影響了,由於對象池中在執行第一句代碼String s = new String("1");的時候已經生成「1」對象了。下邊的s2聲明都是直接從常量池中取地址引用的。 s 和 s2 的引用地址是不會相等的。
  • 小結-從上述的例子代碼能夠看出 jdk7 版本對 intern 操做和常量池都作了必定的修。主要包括2點:
    • 將String常量池 從 Perm 區移動到了 Java Heap區
    • String#intern 方法時,若是存在堆中的對象,會直接保存對象的引用,而不會從新建立對象。

參考:

tech.meituan.com/2014/03/06/…

相關文章
相關標籤/搜索