Java String 綜述(上篇)

摘要:

  Java 中的 String類 是我們日常開發中使用最爲頻繁的一個類,但要想真正掌握的這個類卻不是一件容易的事情。筆者爲了還原String類的真實全貌,先分爲上、下兩篇博文來綜述Java中的String類。筆者從Java內存模型展開,結合 JDK 中 String類的源碼進行深入分析,特別就 String類與享元模式,String常量池,String不可變性,String對象的創建方式,String與正則表達式,String與克隆,String、StringBuffer 和 StringBuilder 的區別等幾個方面對其進行詳細闡述、總結,力求能夠對String類的原理和使用作一個最全面和最準確的介紹。


友情提示:

  應CSDN博友建議,筆者將原文《Java String 綜述》(“特別長的巨文…哈哈”)分爲上下兩篇。上篇(即本文)主要介紹 Java內存模型與常量池、常量與變量、String定義與基礎、String的不可變性 和 String對象創建方式五部分內容;下篇主要介紹字符串常量池、String,StringBuilder 和 StringBuffer 三個字符串類的聯繫和區別、String與正則表達式、String與(深)克隆和String總結五部分內容,煩請各位看官移步《Java String 綜述(下篇)》


版權聲明:

本文原創作者:書呆子Rico
作者博客地址:http://blog.csdn.net/justloveyou_/


一. Java 內存模型 與 常量池

1、Java 內存模型

                  JVM內存模型.bmp-422kB

  • 程序計數器

      多線程時,當線程數超過CPU數量或CPU內核數量,線程之間就要根據時間片輪詢搶奪CPU時間資源。因此,每個線程要有一個獨立的程序計數器,記錄下一條要運行的指令,其爲線程私有的內存區域。如果執行的是JAVA方法,計數器記錄正在執行的java字節碼地址,如果執行的是native方法,則計數器爲空。

  • 虛擬機棧

      線程私有的,與線程在同一時間創建,是管理JAVA方法執行的內存模型。棧中主要存放一些基本類型的變量數據(int, short, long, byte, float, double, boolean, char)和對象引用。每個方法執行時都會創建一個楨棧來存儲方法的的變量表、操作數棧、動態鏈接方法、返回值、返回地址等信息。棧的大小決定了方法調用的可達深度(遞歸多少層次,或嵌套調用多少層其他方法,-Xss參數可以設置虛擬機棧大小)。棧的大小可以是固定的,或者是動態擴展的。如果請求的棧深度大於最大可用深度,則拋出stackOverflowError;如果棧是可動態擴展的,但沒有內存空間支持擴展,則拋出OutofMemoryError。使用jclasslib工具可以查看class類文件的結構。下圖爲棧幀結構圖:

                  虛擬機棧.bmp-442.1kB

  • 本地方法區

      和虛擬機棧功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用 C 實現的。

  • JAVA堆

      線程共享的,存放所有對象實例和數組,是垃圾回收的主要區域。堆是一個運行時數據區,類的對象從中分配空間,這些對象通過new、newarray、 anewarray 和 multianewarray等指令建立,它們不需要程序代碼來顯式的釋放。堆可以分爲新生代和老年代(tenured)。新生代用於存放剛創建的對象以及年輕的對象,如果對象一直沒有被回收,生存得足夠長,老年對象就會被移入老年代。新生代又可進一步細分爲eden(伊甸園)、survivorSpace0(s0,from space)、survivorSpace1(s1,tospace)。剛創建的對象都放入eden,s0和s1都至少經過一次GC並倖存。如果倖存對象經過一定時間仍存在,則進入老年代(tenured)。

                  heap.bmp-174kB

  • 方法區

      線程共享的,用於存放被虛擬機加載的類的元數據信息:如常量、靜態變量、即時編譯器編譯後的代碼,也成爲永久代。如果hotspot虛擬機確定一個類的定義信息不會被使用,也會將其回收。回收的基本條件至少有:所有該類的實例被回收,而且裝載該類的ClassLoader被回收。


2、常量池

  常量池屬於類信息的一部分,而類信息反映到 JVM 內存模型中對應於方法區,也就是說,常量池位於方法區。常量池主要存放兩大常量:字面量(Literal) 和 符號引用(Symbolic References)。其中,字面量主要包括字符串字面量,整型字面量 和 聲明爲final的常量值等;而符號引用則屬於編譯原理方面的概念,包括了下面三類常量:

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

二. 常量與變量

  • 我們一般把內存地址不變,值可以改變的東西稱爲變量,換句話說,在內存地址不變的前提下內存的內容是可變的,例如:
public class String_2 {  
    public static void f(){  
        Human_1 h = new Human_1(1,30);  
        Human_1 h2 = h; 
        System.out.printf("h: %s\n", h.toString());   
        System.out.printf("h2: %s\n\n", h.toString());   

        h.id = 3;  
        h.age = 32;  
        System.out.printf("h: %s\n", h.toString());   
        System.out.printf("h2: %s\n\n", h.toString());   

        System.out.println( h == h2 );   // true : 引用值不變,即對象內存底子不變,但內容改變
    }
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 我們一般把若內存地址不變, 則值也不可以改變的東西稱爲常量典型的 String 就是不可變的,所以稱之爲 常量(constant)。此外,我們可以通過final關鍵字來定義常量,但嚴格來說,只有基本類型被其修飾後纔是常量(對基本類型來說是其值不可變,而對於對象變量來說其引用不可再變)。
    eg:
final int i = 5;
  • 1

三. String 定義與基礎

  1. String 的聲明

              String的定義.png-39.3kB

       
     由 JDK 中關於String的聲明可以知道:

    • 不同字符串可能共享同一個底層char數組,例如字符串 String s=”abc”s.substring(1) 就共享同一個char數組:char[] c = {‘a’,’b’,’c’}。其中,前者的 offset 和 count 的值分別爲0和3,後者的 offset 和 count 的值分別爲1和2。
    • offset 和 count 兩個成員變量不是多餘的,比如,在執行substring操作時。

2、 JDK中關於 String 的描述

 The String class represents character strings. All string literals(字符串字面值) in Java programs, such as “abc”, are implemented as instances of this class. Strings are constant(常量); their values cannot be changed after they are created. String buffers【StringBuilder OR StringBuffer】 support mutable strings. Because String objects are immutable, they can be shared ( 享元模式 ).


3、 String 類所內置的操作

  The class String includes methods for examining individual characters of the sequence for examining individual characters of the sequence, for comparing strings , for searching strings , for extracting substrings and for creating a copy of a string with all characters translated to uppercase or to lowercase. Case mapping is based on the Unicode Standard version specified by the java.lang.Character class.


4、字符串串聯符號(”+”)以及將其他對象轉換爲字符串的特殊支持

  The Java language provides special support for the string concatenation operator (+), and for conversion of other objects to strings. String concatenation is implemented through the StringBuilder(JDK1.5 以後) OR StringBuffer(JDK1.5 以前) class and its append method. String conversions(轉化爲字符串) are implemented through the method toString, defined by class Object and inherited by all classes in Java.


注意:

  • String不屬於八種基本數據類型,String 的實例是一個對象。因爲對象的默認值是null,所以String的默認值也是null;但它又是一種特殊的對象,有其它對象沒有的一些特性(String 的不可變性導致其像八種基本類型一樣,比如,作爲方法參數時,像基本類型的傳值效果一樣)。 例如,以下代碼片段:
public class StringTest {

    public static void changeStr(String str) {
        String s = str;
        str += "welcome";
        System.out.println(s);
    }

    public static void main(String[] args) {
        String str = "1234";
        changeStr(str);
        System.out.println(str);
    }
}/* Output: 
        1234
        1234 
*///:~ 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • new String() 和 new String(“”)都是聲明一個新的空字符串,是空串不是null;

四. String 的不可變性

1、什麼是不可變對象?

  衆所周知,在Java中,String類是不可變類 (基本類型的包裝類都是不可改變的) 的典型代表,也是Immutable設計模式的典型應用。String變量一旦初始化後就不能更改,禁止改變對象的狀態,從而增加共享對象的堅固性、減少對象訪問的錯誤,同時還避免了在多線程共享時進行同步的需要。那麼,到底什麼是不可變的對象呢? 可以這樣認爲:如果一個對象,在它創建完成之後,不能再改變它的狀態,那麼這個對象就是不可變的。不能改變狀態指的是不能改變對象內的成員變量,包括:

  • 基本數據類型的值不能改變;

  • 引用類型的變量不能指向其他的對象;

  • 引用類型指向的對象的狀態也不能改變;

除此之外,還應具有以下特點:

  • 除了構造函數之外,不應該有其它任何函數(至少是任何public函數)修改任何成員變量;

  • 任何使成員變量獲得新值的函數都應該將新的值保存在新的對象中,而保持原來的對象不被修改。


2、區分引用和對象

  對於Java初學者, 對於String是不可變對象總是存有疑惑。看下面代碼:

String s = "ABCabc";
System.out.println("s = " + s);    // s = ABCabc

s = "123456";
System.out.println("s = " + s);    // s = 123456
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

  首先創建一個String對象s,然後讓s的值爲“ABCabc”, 然後又讓s的值爲“123456”。 從打印結果可以看出,s的值確實改變了。那麼怎麼還說String對象是不可變的呢? 其實這裏存在一個誤區: s 只是一個String對象的引用,並不是對象本身。對象在內存中是一塊內存區,成員變量越多,這塊內存區佔的空間越大。引用只是一個 4 字節的數據,裏面存放了它所指向的對象的地址,通過這個地址可以訪問對象。 也就是說,s只是一個引用,它指向了一個具體的對象,當s=“123456”; 這句代碼執行過之後,又創建了一個新的對象“123456”, 而引用s重新指向了這個心的對象,原來的對象“ABCabc”還在內存中存在,並沒有改變。內存結構如下圖所示:

                  對象和對象的引用1.jpg-38.5kB

  Java和C++的一個不同點是,在 Java 中,引用是訪問、操縱對象的唯一方式: 我們不可能直接操作對象本身,所有的對象都由一個引用指向,必須通過這個引用才能訪問對象本身,包括獲取成員變量的值,改變對象的成員變量,調用對象的方法等。而在C++中存在引用,對象和指針三個東西,這三個東西都可以訪問對象。其實,Java中的引用和C++中的指針在概念上是相似的,他們都是存放的對象在內存中的地址值,只是在Java中,引用喪失了部分靈活性,比如Java中的引用不能像C++中的指針那樣進行加減運算。


3、爲什麼String對象是不可變的?

  要理解String的不可變性,首先看一下String類中都有哪些成員變量。 在JDK1.6中,String 的成員變量有以下幾個:

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</string>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

  在JDK1.7中,String類做了一些改動,主要是改變了substring方法執行時的行爲,這和本文的主題不相關。JDK1.7中String類的主要成員變量就剩下了兩個:

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

    /** Cache the hash code for the string */
    private int hash; // Default to 0</string>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

  由以上的代碼可以看出, 在Java中,String類其實就是對字符數組的封裝JDK6中, value是String封裝的數組,offset是String在這個value數組中的起始位置,count是String所佔的字符的個數。在JDK7中,只有一個value變量,也就是value中的所有字符都是屬於String這個對象的。這個改變不影響本文的討論。 除此之外還有一個hash成員變量,是該String對象的哈希值的緩存,這個成員變量也和本文的討論無關。在Java中,數組也是對象(可以參考我之前的文章java中數組的特性)。 所以value也只是一個引用,它指向一個真正的數組對象。其實執行了String s = “ABCabc”; 這句代碼之後,真正的內存佈局應該是這樣的:

                 String本質.jpg-29.4kB

  value,offset和count這三個變量都是 private 的,並且沒有提供setValue,setOffset和setCount等公共方法來修改這些值,所以在String類的外部無法修改String。也就是說一旦初始化就不能修改, 並且在String類的外部不能訪問這三個成員。此外,value,offset和count這三個變量都是final的, 也就是說在String類內部,一旦這三個值初始化了, 也不能被改變。所以,可以認爲String對象是不可變的了。

  那麼在String中,明明存在一些方法,調用他們可以得到改變後的值。這些方法包括substring, replace, replaceAll, toLowerCase等。例如如下代碼:

String a = "ABCabc";
System.out.println("a = " + a);    // a = ABCabc

a = a.replace('A', 'a');
System.out.println("a = " + a);    //a = aBCabc
  • 1
  • 2
  • 3
  • 4
  • 5

  那麼a的值看似改變了,其實也是同樣的誤區。再次說明, a只是一個引用, 不是真正的字符串對象,在調用a.replace(‘A’, ‘a’)時, 方法內部創建了一個新的String對象,並把這個心的對象重新賦給了引用a。String中replace方法的源碼可以說明問題:

            replace源碼.png-31.6kB

  我們可以自己查看其他方法,都是在方法內部重新創建新的String對象,並且返回這個新的對象,原來的對象是不會被改變的。這也是爲什麼像replace, substring,toLowerCase等方法都存在返回值的原因。也是爲什麼像下面這樣調用不會改變對象的值:

String ss = "123456";
System.out.println("ss = " + ss);     // ss = 123456


ss.replace('1', '0');
System.out.println("ss = " + ss);     //ss = 123456
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4、String對象真的不可變嗎?

  從上文可知String的成員變量是 private final 的,也就是初始化之後不可改變。那麼在這幾個成員中, value比較特殊,因爲他是一個引用變量,而不是真正的對象。value是final修飾的,也就是說final不能再指向其他數組對象,那麼我能改變value指向的數組嗎? 比如,將數組中的某個位置上的字符變爲下劃線“_”。 至少在我們自己寫的普通代碼中不能夠做到,因爲我們根本不能夠訪問到這個value引用,更不能通過這個引用去修改數組,那麼,用什麼方式可以訪問私有成員呢? 沒錯,用反射,可以反射出String對象中的value屬性, 進而改變通過獲得的value引用改變數組的結構。下面是實例代碼:

public static void testReflection() throws Exception {

    //創建字符串"Hello World", 並賦給引用s
    String s = "Hello World"; 

    System.out.println("s = " + s); //Hello World

    //獲取String類中的value字段
    Field valueFieldOfString = String.class.getDeclaredField("value");

    //改變value屬性的訪問權限
    valueFieldOfString.setAccessible(true);

    //獲取s對象上的value屬性的值
    char[] value = (char[]) valueFieldOfString.get(s);

    //改變value所引用的數組中的第5個字符
    value[5] = '_';

    System.out.println("s = " + s);  //Hello_World
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

  在這個過程中,s始終引用的同一個String對象,但是再反射前後,這個String對象發生了變化, 也就是說,通過反射是可以修改所謂的“不可變”對象的。但是一般我們不這麼做。這個反射的實例還可以說明一個問題:如果一個對象,他組合的其他對象的狀態是可以改變的,那麼這個對象很可能不是不可變對象。例如一個Car對象,它組合了一個Wheel對象,雖然這個Wheel對象聲明成了private final 的,但是這個Wheel對象內部的狀態可以改變, 那麼就不能很好的保證Car對象不可變。


五. String 對象創建方式

1. 字面值形式: JVM會自動根據字符串常量池中字符串的實際情況來決定是否創建新對象 (要麼不創建,要麼創建一個對象,關鍵要看常量池中有沒有)

JDK 中明確指出:

String s = "abc";
  • 1

等價於:

char data[] = {'a', 'b', 'c'};
String str = new String(data);
  • 1
  • 2

  該種方式先在棧中創建一個對String類的對象引用變量s,然後去查找 “abc”是否被保存在字符串常量池中。若”abc”已經被保存在字符串常量池中,則在字符串常量池中找到值爲”abc”的對象,然後將s 指向這個對象; 否則,中創建char數組 data,然後在中創建一個String對象object,它由 data 數組支持,緊接着這個String對象 object 被存放進字符串常量池,最後將 s 指向這個對象。

例如:

    private static void test01(){  
    String s0 = "kvill";        // 1
    String s1 = "kvill";        // 2
    String s2 = "kv" + "ill";     // 3

    System.out.println(s0 == s1);       // true  
    System.out.println(s0 == s2);       // true  
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

  執行第 1 行代碼時,“kvill” 入池並被 s0 指向;執行第 2 行代碼時,s1 從常量池查詢到” kvill” 對象並直接指向它;所以,s0 和 s1 指向同一對象。 由於 ”kv” 和 ”ill” 都是字符串字面值,所以 s2 在編譯期由編譯器直接解析爲 “kvill”,所以 s2 也是常量池中”kvill”的一個引用。 所以,我們得出 s0==s1==s2;


2. 通過 new 創建字符串對象 : 一概在堆中創建新對象,無論字符串字面值是否相等 (要麼創建一個,要麼創建兩個對象,關鍵要看常量池中有沒有)

String s = new String("abc");  
  • 1

等價於:

1、String original = "abc"; 
2、String s = new String(original);
  • 1
  • 2

  所以,通過 new 操作產生一個字符串(“abc”)時,會先去常量池中查找是否有“abc”對象,如果沒有,則創建一個此字符串對象並放入常量池中。然後,在堆中再創建“abc”對象,並返回該對象的地址。所以,對於 String str=new String(“abc”)如果常量池中原來沒有”abc”,則會產生兩個對象(一個在常量池中,一個在堆中);否則,產生一個對象。
 
  用 new String() 創建的字符串對象位於堆中,而不是常量池中。它們有自己獨立的地址空間,例如,

    private static void test02(){  
    String s0 = "kvill";  
    String s1 = new String("kvill");  
    String s2 = "kv" + new String("ill");  

    String s = "ill";
    String s3 = "kv" + s;    


    System.out.println(s0 == s1);       // false  
    System.out.println(s0 == s2);       // false  
    System.out.println(s1 == s2);       // false  
    System.out.println(s0 == s3);       // false  
    System.out.println(s1 == s3);       // false  
    System.out.println(s2 == s3);       // false  
}  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

  例子中,s0 還是常量池中”kvill”的引用,s1 指向運行時創建的新對象”kvill”,二者指向不同的對象。對於s2,因爲後半部分是 new String(“ill”),所以無法在編譯期確定,在運行期會 new 一個 StringBuilder 對象, 並由 StringBuilder 的 append 方法連接並調用其 toString 方法返回一個新的 “kvill” 對象。此外,s3 的情形與 s2 一樣,均含有編譯期無法確定的元素。因此,以上四個 “kvill” 對象互不相同。StringBuilder 的 toString 爲:

 public String toString() {
    return new String(value, 0, count);   // new 的方式創建字符串
    }
  • 1
  • 2
  • 3

  構造函數 String(String original) 的源碼爲:

    /**
     * 根據源字符串的底層數組長度與該字符串本身長度是否相等決定是否共用支撐數組
     */
    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);  // 創建新數組並賦給 v
        } 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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

  由源碼可以知道,所創建的對象在大多數情形下會與源字符串 original 共享 char數組 。但是,什麼情況下不會共享呢?
  
  Take a look at substring , and you’ll see how this can happen.

  Take for instance String s1 = “Abcd”; String s2 = s1.substring(3). Here s2.size() is 1, but s2.value.length is 4. This is because s1.value is the same as s2.value. This is done of performance reasons (substring is running in O(1), since it doesn’t need to copy the content of the original String).

String s1 = "Abcd";       // s1 的value爲Abcd的數組,offset爲 0,count爲 4
String s2 = a.substring(3);      // s2 的value也爲Abcd的數組,offset爲 3,count爲 1
String c = new String(s2);      // s2.value.length 爲 4,而 original.count = size = 1, 即 s2.value.length > size 成立
  • 1
  • 2
  • 3

  Using substring can lead to a memory leak. Say you have a really long String, and you only want to keep a small part of it. If you just use substring, you will actually keep the original string content in memory. Doing String snippet = new String(reallyLongString.substring(x,y)) , prevents you from wasting memory backing a large char array no longer needed.

詳細可見:
how could ‘originalValue.length > size’ happen in the String constructor?
String構造器中 originalValue.length > size 發生的情況


  更多關於字面量的介紹請移步我的博文《Java 原生類型與包裝器類型深度剖析》

  更多關於享元模式的介紹請移步我的博文《深入理解享元模式》


引用

JVM內存模型及垃圾回收算法
深入理解Java:String
java中特殊的String類型
Java中的String爲什麼是不可變的? – String源碼分析
String,StringBuffer與StringBuilder的區別及應用場景
JAVA中的clone方法剖析
java克隆中String的特殊性
Java堆、棧和常量池以及相關String的詳細講解(經典中的經典)
什麼是字符串常量池?
java中的堆、棧和常量池

        <link rel="stylesheet" href="https://csdnimg.cn/release/phoenix/template/css/markdown_views-ea0013b516.css">
            </div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章