吃透Java基礎十三:String字符串

一、String基礎

1、創建字符串方式

  1. String test = "abc";
  2. String test = new String("abc");

2、String類是不可變的


吃透Java基礎十三:String字符串


  • String類被final關鍵字修飾,意味著String類不能被繼承,並且它的成員方法都默認為final方法;字符串一旦創建就不能再修改。
  • String實例的值是通過字符數組實現字符串存儲的。

String類不可變的好處?

  • 作為HashMap的鍵:因為String是不可變的,當創建String的時候哈希嗎已經計算好了,所以每次在使用字符串的哈希碼的時候就不用再計算一次,更高效。
  • 線程安全:因為字符串是不可變的,所以一定是線程安全的,不用考慮多線程訪問加鎖的情況。
  • 字符串常量池的需要。

3、String類常用方法

  • int length():返回字符串的長度。


吃透Java基礎十三:String字符串

  • int indexOf(int ch, int fromIndex) / int indexOf(String str, int fromIndex):該字符串中查找從fromIndex開始第一次出現ch字符/str字符串的位置。


吃透Java基礎十三:String字符串


  • int lastIndexOf(int ch, int fromIndex) / int lastIndexOf(String str, int fromIndex):查找ch字符/str字符串在該字符串最後一次出現的位置。


吃透Java基礎十三:String字符串


  • String substring(int beginIndex, int endIndex):截取從beginIndex(包含)到endIndex(不包含)的字符串。


吃透Java基礎十三:String字符串


  • String trim():去除字符串中的前後空格


吃透Java基礎十三:String字符串


  • boolean equals(Object anObject):重寫Object中的equals方法,字符串相同則返回true。


吃透Java基礎十三:String字符串


  • String toLowerCase():將字符串轉換為小寫。


吃透Java基礎十三:String字符串


  • String toUpperCase():將字符串轉換為大寫。


吃透Java基礎十三:String字符串


  • char charAt(int index):獲取字符串中指定位置的字符。


吃透Java基礎十三:String字符串


  • String[] split(String regex, int limit):將字符串分割為子字符串,返回字符串數組。


吃透Java基礎十三:String字符串


  • String replace(char oldChar, char newChar):把字符串序列中的oldChar替換為newChar。


吃透Java基礎十三:String字符串


  • byte[] getBytes():將該字符串轉為byte數組。


吃透Java基礎十三:String字符串


二、String高級

1、字符串常量池

字符串的分配和其他對象分配一樣,是需要消耗高昂的時間和空間的,而且字符串使用的非常多。JVM為了提高性能和減少內存的開銷,在實例化字符串的時候進行了一些優化:使用字符串常量池。每當創建字符串常量時,JVM會首先檢查字符串常量池,如果該字符串已經存在常量池中,那麼就直接返回常量池中的實例引用。如果字符串不存在常量池中,就會實例化該字符串並且將其放到常量池中。由於String字符串的不可變性,常量池中一定不存在兩個相同的字符串。

字符串常量池在jdk1.7之前是存在方法去的,從1.7之後放到堆裡面了。

2、String test = "abc"和new String("abc")區別

String test = "abc":首先檢查字符串常量池中是否存在此字符串,如果存在則直接返回字符串常量池中字符串的引用,如果不存在則添加此字符串進常量池然後返回此引用。

new String("abc"):直接在堆中創建字符串返回新創建字符串的引用,每次創建都是一個新的對象。

如下例子:


吃透Java基礎十三:String字符串


3、intern()方法

直接使用雙引號創建出來的String對象會直接存儲在字符串常量池中,new創建的的String對象,可以使用String提供的intern方法。intern 方法是一個native方法,intern方法會從字符串常量池中查詢當前字符串是否存在,如果存在,就直接返回當前字符串;如果不存在就會將當前字符串放入常量池中,之後再返回。

如下例子:


吃透Java基礎十三:String字符串


4、+字符串拼接

使用“+”拼接字符串時,JVM會隱式創建StringBuilder對象,每一個拼接就會隱式創建一個StringBuilder對象,當大量字符串拼接時,就會有大量StringBuilder在堆內存中創建,肯定會造成效率的損失,所以一般大量字符串拼接建議用StringBuilder(線程不安全)或StringBuffer(線程安全)。

如下例子看一下兩者效率:


吃透Java基礎十三:String字符串


運行輸出:


吃透Java基礎十三:String字符串


從結果我們可以看到,10萬次字符串的拼接用“+”的方式耗時13684毫秒,而StringBuilder拼接的話耗時1毫秒,效率是顯而易見的。

當"+"兩端均為編譯期確定的字符串常量時,編譯器會進行相應的優化,直接將兩個字符串常量拼接好

看如下例子:


吃透Java基礎十三:String字符串


編譯後class文件如下:


吃透Java基礎十三:String字符串


可見拼接的兩個字符串如果是常亮則直接在編譯器合併優化。


吃透Java基礎十三:String字符串


5、StringBuilder和StringBuffer拼接字符串

StringBuilder為了解決使用”+“大量拼接字符串時產生很多中間對象問題而提供的一個類,提供append和add方法,可以將字符串添加到已有序列的末尾或指定位置,實際上底層都是利用可修改的char數組(JDK 9 以後是 byte數組)來存儲的,當前容量不足時觸發擴容機制。

StringBuffer和StringBuilder的機制一樣,唯一不同點是StringBuffer內部使用synchronized關鍵字來保證線程安全,保證線程安全不可避免的產生了一些額外的開銷,如果不要求線程安全的情況下還是建議優先使用StringBuilder。

StringBuilder和StringBuffer都是繼承AbstractStringBuilder抽象類,裡面實現了字符串拼接的具體邏輯,我們來看一下:


吃透Java基礎十三:String字符串


數據都是保存在char[]裡面,當容量不足時進行數組的擴容:


吃透Java基礎十三:String字符串


從源碼可以看到,當需要容量不足時觸發擴容機制,擴容為原來的2倍+2的容量,最大數組擴容容量為:MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,Integer.MAX_VALUE為2的31次方-1。擴容完後把老的數組裡面的數據拷貝到新的數組裡面。

6、String字符串長度限制

要回答這個問題要分兩個階段看,分別是編譯期和運行期。不同的時期限制不一樣。

編譯期

我們所能想到的是String源碼中定義,存儲String字符的char數組最大是Integer.MAX_VALUE為2的31次方-1,那麼也就是說可以存儲2147483647個字符,分析到這時是不是以為大功告成了,其實不然,我們忽略了一點。

我們編譯java文件為class文件的時候是把字符串放在class的常量池中的,JVM對常量池的容量有嚴格的限制,JVN規定,字符串是由CONSTANTUtf8info類型的結構,CONSTANTUtf8_info的定義如下:


吃透Java基礎十三:String字符串


其中u1、u2是無符號的分別代表1個字節和2個字節,那麼我們只需要看length最多允許兩個字節的長度,因此理論上允許的的最大長度是2^16=65536。而 java class 文件是使用一種變體UTF-8格式來存放字符的,null 值使用兩個 字節來表示,因此只剩下 65536- 2 = 65534個字節。

所以,在Java中,所有需要保存在常量池中的數據,長度最大不能超過65535,這當然也包括字符串的定義。

運行期

上面提到的這種String長度的限制是編譯期的限制,也就是使用String str= "";這種字面值方式定義的時候才會有的限制。

String在運行期的限制,其實就是我們前文提到的那個Integer.MAX_VALUE ,這個值約等於4G,在運行期,如果String的長度超過這個範圍,就會拋出異常。


分享到:


相關文章: