String StringBuffer StringBuilder

本文章還是得提到String Immutable 特性。這樣才有利於String 、StringBuffer 、StringBuilder 特點的比較。

以下基於 java for windows

java version “1.8.0_171”
Java™ SE Runtime Environment (build 1.8.0_171-b11)
Java HotSpot™ 64-Bit Server VM (build 25.171-b11, mixed mode)

圖中示例代碼註釋只是爲了展示、對比方便,所以放在代碼尾部

String

String Immutable 特性

以下爲 java.lang.String 屬性字段 

    /** The value is used for character storage. */
    private final char value[];

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

String 類 implements java.io.Serializable, Comparable< String >, CharSequence。類是用final修飾的。且其屬性字段爲value未提供set修改其對象內部值。首先要了解一點

  • 每次新建String類型對象 其值(方法區 String Pool 、堆區) 都是不可變的(immutable)。最直觀代碼如下

    // 這種行爲是在 String pool 創建對象,其引用存在棧中
    String str1 = "hello world";
    // 使用此種方法重新創建String對象時 java會首先在String pool中檢查是否存在  hello world 存在即返回其引用 所以 str1、str2 指向同一對象
    String str2 = "hello world";
    
    System.out.println("result:" + (str1 == str2)); // result:true
    
    // 此種方式創建的String對象位於堆內 每次創建都指向一個新的對象 各自獨立的堆空間
    String str3 = new String("hello world");
    String str4 = new String("hello world");
    
    
    System.out.println("result2:" + (str3 == str4));// result2:false
    
    // 在堆中的String不同於在String pool 的String
    System.out.println("result3:" + (str2 == str4)); // result3:false
    
    // 此操作在Pool中重新創建String 其引用改變 但原  hello world 未變
    str1 = "hello Man";
    
    System.out.println("change1: " + str1);// change1: hello Man
    System.out.println("change1: " + str2);// change1: hello world
    
  • String immutable特性帶來很多便利

    • 不可變對象可以提高String Pool的效率和安全性。如果你知道一個對象是不可變的,那麼需要拷貝這個對象的內容時,就不用複製它的本身而只是複製它的地址,複製地址(通常一個指針的大小)需要很小的內存效率也很高。對於同時引用這個“hello world” 的其他變量也不會造成影響。節省空間,加快效率。
    • 不可變對象對於多線程是安全的,因爲在多線程同時進行的情況下,一個可變對象的值很可能被其他進程改變,這樣會造成不可預期的結果,而使用不可變對象就可以避免這種情況。(最直觀的例子就是 String被廣泛的作爲方法的參數,比如打開網絡連接時傳遞主機名和端口號作爲參數,傳遞數據庫URL用於連接數據庫,也可以在打開文件時傳遞文件名給Java的I/O class。如果String不是immutable,會帶來嚴重的安全問題,假設一個人對其授權訪問的文件,改變了文件名並重新獲了文件訪問權限,但是因爲String是immutable,你就不需要擔心這種情況。這也可以解釋爲啥String 是final的,final string保證了任何人都不能重寫String而改變String的行爲。)
    • 不可變性,導致其是線程安全的。
    • 支持hash映射和緩存(此點在之後的文章中會提到)

反射修改了String的值?

JAVA不可變類(immutable)機制與String的不可變性 中 看到博主的一段說明:

發現String的值已經發生了改變。也就是說,通過反射是可以修改所謂的“不可變”對象的

特以下代碼驗證此點

String str1 = "hello world";
String str2 = "hello world";
System.out.println("result1:" + (str1 == str2)); // result1:true


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

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

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

//改變value所引用的數組中的第5個字符
value[5] = 'X';
System.out.println("str1 changed = " + str1);  // str1 changed = helloXworld

System.out.println("str2 = " +str2);// str2 = helloXworld

結果顯而易見:改了裏面的值,並且執行其值的 str2 值也被修改


但是,如果將 str1 使用final修飾,輸出的結果將是:

str1 changed = hello world
str2 = helloXworld

也就是說 str1 的值並未改變

爲什麼會出現這種情況?請見下文。

這是我的java類 .java(str1未被final修飾前)

import java.lang.reflect.Field;

/**
 * @Description:
 * @Author: amarone
 */
public class Test {

    public static void main(String[] args) throws Exception{
        String str1 = "hello world";
        String str2 = "hello world";
        System.out.println("result1:" + (str1 == str2));


       
        Field valueFieldOfString = String.class.getDeclaredField("value");

      
        valueFieldOfString.setAccessible(true);

    
        char[] value = (char[]) valueFieldOfString.get(str1);

      
        value[5] = 'X';
		
        System.out.println("str1 changed = " + str1);  //str1 changed = helloXworld

        System.out.println("str2 = " +str2); // str2 = helloXworld
    }

}

這是經過javac 編譯.java 後的 .class 。 通過 jd-gui.exe 反編譯 (str1未被final修飾前)

import java.io.PrintStream;
import java.lang.reflect.Field;

public class Test
{
  public static void main(String[] paramArrayOfString)
    throws Exception
  {
    String str1 = "hello world";
    String str2 = "hello world";
    System.out.println("result1:" + (str1 == str2));
    


    Field localField = String.class.getDeclaredField("value");
    

    localField.setAccessible(true);
    

    char[] arrayOfChar = (char[])localField.get(str1);
    

    arrayOfChar[5] = 'X';
    
    System.out.println("str1 changed = " + str1);
    
    System.out.println("str2 = " + str2);
  }
}

這是經過javac 編譯.java 後的 .class 。 通過 jd-gui.exe 反編譯 (str1被final修飾)

import java.io.PrintStream;
import java.lang.reflect.Field;

public class Test
{
  public static void main(String[] paramArrayOfString)
    throws Exception
  {
    String str = "hello world";
    System.out.println("result1:" + ("hello world" == str));
    


    Field localField = String.class.getDeclaredField("value");
    

    localField.setAccessible(true);
    

    char[] arrayOfChar = (char[])localField.get("hello world");
    

    arrayOfChar[5] = 'X';
    
    System.out.println("str1 changed = hello world");
    
    System.out.println("str2 = " + str);
  }
}

可以注意到: 在加上final 修飾之後,java文件被編譯爲class文件時

(這也是我上篇文章《(四)Final 關鍵字》 中提到部分有涉及的 編譯期優化 )

System.out.println("str1 changed = " + str1); ==> System.out.println("str1 changed = hello world");

輸出的不是引用的變量而是一個字符串!也就是說在編譯器就已經優化了,變量再怎麼改都沒用。final修飾Stirng是爲了防止繼承String暴露Stirng內部屬性。

爲什麼要有StringBuffer或者StringBuilder?

Stirng 字符串拼接時

import java.io.PrintStream;

public class Test
{
  public static void main(String[] paramArrayOfString)
    throws Exception
  {
    String str1 = "aaa";
    
    String str2 = str1 + "b";
    
    System.out.println(str1);
    System.out.println(str2);
    System.out.println(str1 + str2);
  }
}

str1、str2、"b" 都是單獨的對象,如果存在大量的字符串拼接,那麼就會大量創建對象,然後你的新生代(倖存者區)很快就滿了80% 然後就觸發GC了。或者就直接掛了。

爲了應對大量拼接字符串拼接,java提供了 StringBuffer 類。

 public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence
{
    **
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    static final long serialVersionUID = 3388685877147921107L;
    
    ········
}

StringBuffer 繼承於 AbstractStringBuilder

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;
}

StringBuffer 提供了append() 方法用以在指定位置添加字符串

   @Override
    public synchronized StringBuffer append(Object obj) {
        toStringCache = null;
        super.append(String.valueOf(obj));
        return this;
    }

    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

可以看到幾乎所有對字符串操作的方法全部添加了synchronized關鍵字。保證了線程安全,但是需要付出部分性能代價。

參考鏈接

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