Java基礎系列2:深入理解String類


Java基礎系列2:深入理解String類

String是Java中最為常用的數據類型之一,也是面試中比較常被問到的基礎知識點,本篇就聊聊Java中的String。主要包括如下的五個內容:

  • String概覽
  • “+”連接符解析
  • 字符串常量池
  • String.intern()方法解析
  • String、StringBuffer與StringBuilder

String概覽

在Java中,所有類似“ABCabc”的字面值,都是String的實例;String類位於java.lang包下,是Java語言的核心類,提供了字符串的比較、查找、截取、大小寫轉換等操作;Java語言為“+”連接符以及對象轉換為字符串提供了特殊支持,字符串對象可以使用“+”連接其他對象。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 ... }

從上面的源碼可以看出:

  1. String類被final關鍵字修飾,意味着String類時不可變類,不能被繼承,並且其成員value也是final的,因此字符串一旦創建就不能再修改;
  2. String類實現了Serializable、CharSequence、Comparable接口;
  3. String實例的值是通過字符數組實現字符串存儲的。

“+”連接符解析

“+”連接符的實現原理

Java語言為“+”連接符以及對象轉換為字符串提供了特殊的支持。其中字符串連接是通過StringBuilder及其append方法實現的,對象轉換字符串是通過toString方法實現的,toString方法由Object類實現,並可被Java中的所有類繼承。用個簡單的例子來驗證“+”連接符的實現原理:

// 測試代碼
public class Test { public static void main(String[] args) { int i = 2; String str = "abc"; System.out.println(str + i); } } // 反編譯后 public class Test { public static void main(String args[]) { byte byte0 = 10;  String s = "abc";  System.out.println((new StringBuilder()).append(s).append(byte0).toString()); } }

由反編譯后的代碼可以看出,Java使用“+”連接字符串對象時,JVM會創建一個StringBuilder對象,並調用其append方法將字符串連接,最后調用StringBuilder對象的toString方法返回拼接好的字符串。所以在實際代碼編寫中,使用“+”來拼接字符串和使用StringBuilder對象的append方法來拼接字符串對象是等價的。

“+”連接符的注意事項

“+”的效率

使用“+”連接符時,JVM會隱式創建StringBuilder對象,這種方式在大部分情況下並不會造成效率的損失,不過在進行大量循環拼接字符串時則需要注意。因為大量StringBuilder創建在堆內存中,必然會造成效率的損失,所以這種情況建議在循環體外創建一個StringBuilder對象調用append方法手動拼接。

字符串常量的優化

編譯時可以解析為常量值還有一種特殊情況,當“+”兩端均為編譯器確定的字符串常量時,編譯器會進行優化,直接將兩個字符串拼接好。例如:

String s = "hello" + "world!"; // 反編譯后 String s0 = "helloworld!";
/**  * 編譯期確定  * 對於final修飾的變量,它在編譯時被解析為常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的字節碼流中。  * 所以此時的"a" + s1和"a" + "b"效果是一樣的。故結果為true。  */ String s0 = "ab";  final String s1 = "b";  String s2 = "a" + s1;  System.out.println((s0 == s2)); // true 

編譯時不可以被解析為常量值

/**  * 編譯期無法確定  * 這里面雖然將s1用final修飾了,但是由於其賦值是通過方法調用返回的,那么它的值只能在運行期間確定  * 因此s0和s2指向的不是同一個對象,故上面程序的結果為false。  */ String s0 = "ab";  final String s1 = getS1();  String s2 = "a" + s1;  System.out.println((s0 == s2)); // false  public String getS1() {  return "b";  }

綜上,“+”連接符對於直接相加的字符串常量效率很高,因為在編譯期間便確定了它的值,也就是說形如"hello"+"java"; 的字符串相加,在編譯期間便被優化成了"Ilovejava"。對於間接相加(即包含字符串引用,且編譯期無法確定值的),形如s1+s2+s3; 效率要比直接相加低,因為在編譯器不會對引用變量進行優化。

字符串常量池

字符串常量池介紹

在Java語言中的8種基本類型和String類型,JVM都為它們提供了一種常量池的概念,常量池就類似於一個Java系統級別提供的緩存。8種基本類型的常量池都是系統協調的,String類型的常量池比較特殊,它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的String對象會直接存儲在常量池中;
  • 如果不是雙引號聲明的String對象,可以使用String提供的intern方法。intern方法是個Native方法,會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。

由於String字符串的不可變性,常量池中一定不存在兩個相同的字符串。

內存區域

在HotSpot VM中字符串常量池是通過一個StringTable類實現的,它是一個Hash表,默認值大小長度是1009;這個StringTable在每個HotSpot VM的實例中只有一份,被所有的類共享;字符串常量由一個一個字符組成,放在了StringTable上。要注意的是,如果放進String Pool的String非常多,就會造成Hash沖突嚴重,從而導致鏈表會很長,而鏈表長了后直接會造成的影響就是當調用String.intern時性能會大幅下降(因為要一個一個找)。在JDK6及之前版本,字符串常量池是放在Perm Gen區(也就是方法區)中的,StringTable的長度是固定的1009;在JDK7版本中,字符串常量池被移到了堆中,StringTable的長度可以通過-XX:StringTableSize=66666參數指定。至於JDK7為什么把常量池移動到堆上實現,原因可能是由於方法區的內存空間太小且不方便擴展,而堆的內存空間比較大且擴展方便。

內存的分配

在JDK6及之前版本中,String Pool里放的都是字符串常量;在JDK7.0中,由於String.intern()發生了改變,因此String Pool中也可以存放放於堆內的字符串對象的引用。請看如下代碼:

String s1 = "ABC"; String s2 = "ABC"; String s3 = new String("ABC"); System.out.println(s1 == s2); // true System.out.println(s1 == s3); // false System.out.println(s1.intern() == s3.intern()); // true

由於常量池中不存在兩個相同的對象,所以s1和s2都是指向JVM字符串常量池中的"ABC"對象。new關鍵字一定會產生一個對象,並且這個對象存儲在堆中。所以String s3 = new String(“ABC”);產生了兩個對象:保存在棧中的s3和保存在堆中的String對象。當執行String s1 = "ABC"時,JVM首先會去字符串常量池中檢查是否存在"ABC"對象,如果不存在,則在字符串常量池中創建"ABC"對象,並將"ABC"對象的地址返回給s1;如果存在,則不創建任何對象,直接將字符串常量池中"ABC"對象的地址返回給s1。由於s1,s2,s3的字符串值都是在常量池中的同一個引用,所以intern()方法的返回值是相等的。

String.intern()方法解析

String.intern()方法解析

先來看一下String.intern()方法的代碼和注釋:

/**  * Returns a canonical representation for the string object.  * <p>  * A pool of strings, initially empty, is maintained privately by the  * class {@code String}.  * <p>  * When the intern method is invoked, if the pool already contains a  * string equal to this {@code String} object as determined by  * the {@link #equals(Object)} method, then the string from the pool is  * returned. Otherwise, this {@code String} object is added to the  * pool and a reference to this {@code String} object is returned.  * <p>  * It follows that for any two strings {@code s} and {@code t},  * {@code s.intern() == t.intern()} is {@code true}  * if and only if {@code s.equals(t)} is {@code true}.  * <p>  * All literal strings and string-valued constant expressions are  * interned. String literals are defined in section 3.10.5 of the  * <cite>The Java&trade; Language Specification</cite>.  *  * @return a string that has the same contents as this string, but is  * guaranteed to be from a pool of unique strings.  */ public native String intern();

直接使用雙引號聲明出來的String對象會直接存儲在字符串常量池中,如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern 方法是一個native方法,intern方法會從字符串常量池中查詢當前字符串是否存在,如果存在,就直接返回當前字符串;如果不存在就會將當前字符串放入常量池中,之后再返回。JDK1.7的改動將String常量池 從 Perm 區移動到了 Java Heap區String.intern() 方法時,如果存在堆中的對象,會直接保存對象的引用,而不會重新創建對象。

String.intern()的使用

來看看使用和不使用intern()的執行過程,在用new String("ABC")實例化String對象的時候,如果使用了intern方法,那么會先去字符串常量池中去查找是否有值為"ABC"的字符串,找到了就不會創建新的"ABC"字符串,找不到才會去創建新的"ABC"字符串;如果不使用intern方法,則沒有去常量池查找的過程,會直接創建新的"ABC"字符串。可以看出二者的區別是:

  • 使用intern(),實際創建的對象數目是少於需要創建的對象數目的,因為會有常量池的字符串共享;但相應的,所需要的常量池的查詢消耗會增加時間損耗;這體現出的是一種空間友好,不需要太多gc來回收空間;
  • 不使用intern(),實際需要多少對象,就會創建多少對象,因此會有大量的重復值的String對象出現;但相應的,少了查詢的消耗,時間損耗會少一些;這體現出的是一種時間友好。

String、StringBuffer與StringBuilder

類圖

主要區別

  • String是不可變字符序列,StringBuilder和StringBuffer是可變字符序列;
  • StringBuilder是非線程安全的,StringBuffer是線程安全的,其線程安全是通過在成員方法上添加synchronized關鍵字來實現的;
  • 執行效率上,StringBuilder > StringBuffer > String

總結

綜上,我們再通過一個例子來測驗以上的學習成果:

String s1 = "AB"; String s2 = new String("AB"); String s3 = "A"; String s4 = "B"; String s5 = "A" + "B"; String s6 = s3 + s4; System.out.println(s1 == s2); // false System.out.println(s1 == s2.intern()); // true System.out.println(s1 == s5); // true System.out.println(s1 == s6); // false System.out.println(s1 == s6.intern()); // true

要理解此題目,需要搞清楚以下三點:

  1. 直接使用雙引號聲明出來的String對象會直接存儲在常量池中;
  2. String對象的intern方法會得到字符串對象在常量池中對應的引用,如果常量池中沒有對應的字符串,則該字符串將被添加到常量池中,然后返回常量池中字符串的引用;
  3. 字符串的+操作其本質是創建了StringBuilder對象進行append操作,然后將拼接后的StringBuilder對象用toString方法處理成String對象。

看一下以上的6個String對象在內存的分布情況:

 

 

 

 

【參考資料】https://tech.meituan.com/2014/03/06/in-depth-understanding-string-intern.htmlhttps://docs.oracle.com/javase/8/docs/api/https://blog.csdn.net/ifwinds/article/details/80849184

關注我的公眾號,獲取更多關於面試、技術的文章及福利資源。

Dali王的技術博客公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM