Java詳解【String】+【StringBuilder vs StringBuffer】+【字符串拼接】

String詳解

  • 注意區分對象和對象的引用

首先來看一下我在jdk中找到的String源代碼,這裏只截取開頭的小小一部分

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

從這裏可以看出,String類是被final所修飾的,因此String類對象不可變,也不可繼承。這裏要注意一個誤區字符串對象不可變,但字符串變量所指的值是可變的,即引用地址可變。String變量存儲的是對String對象的引用,String對象裏存儲的纔是字符串的值【注意區分對象和對象的引用】。看下面的例子

String str = "abc"; //str是個對象引用
System.out.println(str); //輸出abc
str = "abcde"; //不會出錯
System.out.println(str); //輸出abcde

當給str第二次賦值的時候,對象"abc"並沒有被銷燬,仍存放在常量池中(String自帶),只是讓str指向了"abcde"的內存地址,而字符串對象"abcde"是新生成的,即str第二次賦值時並不是在原內存地址上修改數據,而是重新指向一個新對象,新地址。記住,對String對象的任何改變都不影響到原對象,相關的任何改變的操作都會生成新的對象。

 

  • 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類獲取成員變量
    Field valueFieldOfString = String.class.getDeclaredField("value");
    valueFieldOfString.setAccessible(true); // 改變value屬性的訪問權限
    value[5] = '_'; // 獲取s對象上的value屬性的值 改變value所引用的數組中的第5個字符
    System.out.println("s = " + s); // 打印出Hello_World
}

(此例子參考了博文:https://blog.csdn.net/zhangjg_blog/article/details/18319521

 

下面這個例子將引出兩個問題

  1. String str="hello world" 和 String str=new String("hello world") 有什麼不同
  2. 爲什麼使用 "==" 和 "equals" 會有不同的結果
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");
    String str5 = "Hello" + " World";
    String str6 = "Hello" + new String(" World");
    String str7 = str2.intern();
    String ex1 = "Hello";
    String ex2 = " World";
    String str8 = ex1 + ex2;
         
    System.out.println(str1 == str3);       //true
    System.out.println(str1 == str2);       //false
    System.out.println(str2 == str4);       //false
    System.out.println(str1.equals(str2));  //true
    System.out.println(str2.equals(str4));  //true
    System.out.println(str1 == str5);       //true
    System.out.println(str1 == str6);       //false
    System.out.println(str1 == str7);       //true
    System.out.println(str1 == str8);       //false
}

【運行結果】是 true、false、false、true、true、true、false、true、false

先把下面四大點看懂了,我會在最後寫出【解析】
 

  • String的兩種賦值方式

區分【String str="HW"】和【String str=new String("HW")】

(1)字面量賦值方式     eg:String str = "Hello";

  • 該種直接賦值的方法,JVM會去字符串常量池(String對象不可變)中尋找是否有equals("Hello")的String對象,如果有,就把該對象在字符串常量池中"Hello"的引用複製給字符串變量str,如若沒有,就在堆中新建一個對象,同時把引用駐留在字符串常量池中,再把引用賦給字符串變量str。
  • 用該方法創建字符串時,無論創建多少次,只要字符串的值(內容)相同,那麼它們所指向的都是堆中的同一個對象。
  • 該方法直接賦值給變量的字符串存放在常量池

(2)new關鍵字創建新對象     eg:String str = new String("Hello");

  • 利用new來創建字符串時,無論字符串常量池中是否有與當前值相同的對象引用,都會在中新開闢一塊內存,創建一個新的對象
     

注意:對字符串進行拼接操作,即做"+"運算的時候,分2種情況:

  1. 表達式右邊是純字符串常量,那麼存放在常量池裏面。eg:String str = "Hello" + "World";
  2. 表達式右邊如果存在字符串引用,也就是字符串對象的句柄,那麼就存放在堆裏面。eg:String str = str1 + str2;

總結:常量池是方法區的一部分,而方法區是線程共享的,所以常量池也是線程共享的,且它是線程安全的,它讓有相同值的引用指向同一個位置,如果引用值變化了,但是常量池中沒有新的值,那麼就會新建一個常量結果來交給新的引用,對於同一個對象,new出來的字符串存放在中,而直接賦值給變量的字符串存放在常量池裏。
​​​​​​

  • 字符串比較——區分"=="和"equals"

"==":比較引用變量的地址,即兩個對象是否引用同一地址的內容,用"=="時會檢測是否指向同一個對象

"equals":比較對象的內容,即兩個對象內容上是否相同

字符串用這兩種比較方式都是可行的,具體看想要比較什麼,總體來看,"=="稍微強大些,因爲它既要求內容相同,也要求引用對象相同
 

  • intern() 方法

當使用 intern() 方法時,會先查詢字符串常量池是否存在當前字符串,如果存在,則返回常量池中的引用,若不存在,則將字符串添加到字符串常量池中,並返回字符串常量池中的引用。

 

【代碼解析】

str1 == str3——str1與str3指向常量池中同一個對象,引用對象相同,因此用"=="比較時結果爲true
str1 == str2——new會創建一個新的對象放在堆中,str1所指對象在常量池,即使str1與str2內容相同,但並不是同一個對象
str2 == str4——new會創建一個新的對象放在堆中,str2與str4指向不同的對象,即使內容相同
str1.equals(str2)——str1於str2各自引用對象不同,但內容相同,因此用"equals"比較時結果爲true
str2.equals(str4)——str2於str4各自引用對象不同,但內容相同,因此用"equals"比較時結果爲true
str1 == str5——對字符串進行拼接操作("+"運算)時,表達式右邊是純字符串常量,那麼存放在常量池裏面,若常量池中有該字符串,則返回該引用
str1 == str6——對字符串進行拼接操作("+"運算)時,表達式右邊如果存在字符串引用,也就是字符串對象的句柄,那麼就存放在堆裏面
str1 == str7——intern()方法會去常量池中找是否存在當前字符,存在則返回引用,該對象引用剛好是str1所指向的
str1 == str8——str8由兩個變量拼接,編譯期不知道它們的具體位置,所以不會做出優化,必須要等到運行時才能確定,因此新對象的地址和前面的不同。

 

StringBuilder  & StringBuffer

StringBuilder和StringBuffer類類似於String類,但區別在於String創建的對象是不可改變的,而StringBuilder和StringBuffer這兩個類創建對象後都是可以對對象進行修改的。即String爲字符串常量,而StringBuilder和StringBuffer均爲字符串變量

  • 運行速度(快到慢):StringBuilder > StringBuffer > String

StringBuilder和StringBuffer的對象是變量,對變量進行操作就是直接對該對象進行更改,而不進行像String對象那樣子進行創建和回收的操作,所以速度要比String快很多。

  • 在線程安全上,StringBuilder是線程不安全的,而StringBuffer是線程安全

如果一個StringBuffer對象在字符串緩衝區被多個線程使用時,StringBuffer中很多方法可以帶有synchronized關鍵字,所以可以保證線程是安全的,但StringBuilder的方法則沒有該關鍵字,所以不能保證線程安全,有可能會出現一些錯誤的操作。所以如果要進行的操作是多線程的,那麼就要使用StringBuffer,但是在單線程的情況下,還是建議使用速度比較快的StringBuilder。

 

總結

  • String:適用於少量的字符串操作的情況
  • StringBuilder:適用於線程下在字符緩衝區進行大量操作的情況
  • StringBuffer:適用線程下在字符緩衝區進行大量操作的情況

 

字符串拼接五種方法

  • 使用 +
  • 使用 concat
  • 使用 StringBuilder
  • 使用 StringBuffer
  • 使用 StringUtils.join

注:先看完上面對String、StringBuilder和StringBuffer的詳解後再看下面的文章會好理解很多的

由於String是Java中一個不可變的類,所以他一旦被實例化就無法被修改,因此所有的所謂字符串拼接,都是重新生成了一個新的字符串。

  • 效率(用時短到長):StringBuilder < StringBuffer < concat < < StringUtils.join

(1)"+"是Java提供的一個語法糖,而使用+拼接的字符串,它將String轉成了StringBuilder後,再使用StringBuilder.append進行處理。如果不是在循環體中進行字符串拼接的話,直接使用+就好了。

(2)concat方法,其實是new了一個新的String

(3)StringUtils.join也是通過StringBuilder來實現的

(4)StringBufferStringBuilder的基礎上,做了同步處理,所以在耗時上會相對多一些。

(5)如果在併發場景中進行字符串拼接的話,要使用StringBuffer來替代StringBuilder。因爲StringBuilder是線程不安全的,而StringBuffer是線程安全

更多具體的請參考這篇文章:https://blog.csdn.net/hollis_chuang/article/details/86505501(推薦——字符串拼接)

 

其他參考文章:

https://www.cnblogs.com/su-feng/p/6659064.html

https://www.cnblogs.com/dolphin0520/p/3778589.html

https://www.cnblogs.com/justcooooode/p/7603381.html

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