下面這段代碼建立了幾個對象?java
String a="ab"+"cd";編程
緊接着這段代碼以後的每每是這個問題,那就是這行代碼究竟建立了幾個
String對象呢?相信你們對這道題並不陌生,答案也是衆所周知的,2個。接下來咱們就從這道題展開,一塊兒回顧一下與建立String對象相關的一些JAVA知識。
咱們能夠把上面這行代碼分紅String str、=、"abc"和new String()四部分來看待。String str只是定義了一個名爲str的String類型的變量,所以它並無建立對象;=是對變量str進行初始化,將某個對象的引用(或者叫句柄)賦值給它,顯然也沒有建立對象;如今只剩下new String("abc")了。那麼,new String("abc")爲何又能被當作"abc"和new String()呢?咱們來看一下被咱們調用了的String的構造器:數組
public String(String original) { //other code ... }
你們都知道,咱們經常使用的建立一個類的實例(對象)的方法有如下兩種: 測試
1. 使用new建立對象。
2. 調用Class類的newInstance方法,利用反射機制建立對象。
咱們正是使用new調用了String類的上面那個構造器方法建立了一個對象,並將它的引用賦值給了str變量。同時咱們注意到,被調用的構造器方法接受的參數也是一個String對象,這個對象正是"abc"。由此咱們又要引入另一種建立String對象的方式的討論——引號內包含文本。 code
這種方式是String特有的,而且它與new的方式存在很大區別。對象
String str ="abc";
上面這行代碼建立了一個String對象字符串
String a = "abc"; String b = "abc";
上面這兩行代碼也建立了一個String對象源碼
String a ="ab"+"cd";
上面這行代碼仍然只建立一個String對象虛擬機
答案還是一個。有點奇怪嗎?說到這裏,咱們就須要引入對字符串池相關知識的回顧了。
在JAVA虛擬機(JVM)中存在着一個字符串池,其中保存着不少String對象,而且能夠被共享使用,所以它提升了效率。因爲String類是final的,它的值一經建立就不可改變,所以咱們不用擔憂String對象共享而帶來程序的混亂。字符串池由String類維護,咱們能夠調用intern()方法來訪問字符串池。
咱們再回頭看看String a="abc";,這行代碼被執行的時候,JAVA虛擬機首先在字符串池中查找是否已經存在了值爲"abc"的這麼一個對象,它的判斷依據是String類equals(Object obj)方法的返回值。若是有,則再也不建立新的對象,直接返回已存在對象的引用;若是沒有,則先建立這個對象,而後把它加入到字符串池中,再將它的引用返回。所以,咱們不難理解前面三個例子中頭兩個例子爲何是這個答案了。編譯
對於String a = "ab"+"cd";
因爲常量的值在編譯的時候就被肯定了。在這裏,"ab"和"cd"都是常量,所以變量a的值在編譯時就能夠肯定。這行代碼編譯後的效果等同於:
String a ="abcd";
所以只建立了一個對象「abcd」,而且它將被保存在字符串池中。
如今問題又來了,是否是全部通過「+」鏈接後獲得的字符串都會被添加到字符串池中呢?咱們都知道「==」能夠用來比較兩個變量,它有如下兩種狀況:
1. 若是比較的是兩個基本類型(char,byte,short,int,long,float,double,boolean),則是判斷它們的值是否相等。
2. 若是表較的是兩個對象變量,則是判斷它們的引用是否指向同一個對象。
下面咱們就用「==」來作幾個測試。爲了便於說明,咱們把指向字符串池中已經存在的對象也視爲該對象被加入了字符串池:
public static void main(String[] args) { String a = "ab";// 建立了一個對象,並加入字符串池中 System.out.println("String a = \"ab\";"); String b = "cd";// 建立了一個對象,並加入字符串池中 System.out.println("String b = \"cd\";"); String c = "abcd";// 建立了一個對象,並加入字符串池中 String d = "ab" + "cd"; // 若是d和c指向了同一個對象,則說明d也被加入了字符串池 if (d == c) { System.out.println("\"ab\"+\"cd\" 建立的對象 \"加入了\" 字符串池中"); } // 若是d和c沒有指向了同一個對象,則說明d沒有被加入字符串池 else { System.out.println("\"ab\"+\"cd\" 建立的對象 \"沒加入\" 字符串池中"); } String e = a + "cd"; // 若是e和c指向了同一個對象,則說明e也被加入了字符串池 if (e == c) { System.out.println(" a +\"cd\" 建立的對象 \"加入了\" 字符串池中"); } // 若是e和c沒有指向了同一個對象,則說明e沒有被加入字符串池 else { System.out.println(" a +\"cd\" 建立的對象 \"沒加入\" 字符串池中"); } String f = "ab" + b; // 若是f和c指向了同一個對象,則說明f也被加入了字符串池 if (f == c) { System.out.println("\"ab\"+ b 建立的對象 \"加入了\" 字符串池中"); } // 若是f和c沒有指向了同一個對象,則說明f沒有被加入字符串池 else { System.out.println("\"ab\"+ b 建立的對象 \"沒加入\" 字符串池中"); } String g = a + b; // 若是g和c指向了同一個對象,則說明g也被加入了字符串池 if (g == c) { System.out.println(" a + b 建立的對象 \"加入了\" 字符串池中"); } // 若是g和c沒有指向了同一個對象,則說明g沒有被加入字符串池 else { System.out.println(" a + b 建立的對象 \"沒加入\" 字符串池中"); } } }
運行結果以下:
String a = "ab";
String b = "cd";
"ab"+"cd" 建立的對象 "加入了" 字符串池中
a +"cd" 建立的對象 "沒加入" 字符串池中
"ab"+ b 建立的對象 "沒加入" 字符串池中
a + b 建立的對象 "沒加入" 字符串池中
從上面的結果中咱們不難看出,只有使用引號包含文本的方式建立的String對象之間使用「+」鏈接產生的新對象纔會被加入字符串池中。對於全部包含new方式新建對象(包括null)的「+」鏈接表達式,它所產生的新對象都不會被加入字符串池中,對此咱們再也不贅述。
可是有一種狀況須要引發咱們的注意。請看下面的代碼:
public class StringStaticTest { // 常量A public static final String A = "ab"; // 常量B public static final String B = "cd"; public static void main(String[] args) { // 將兩個常量用+鏈接對s進行初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等於t,它們是同一個對象"); } else { System.out.println("s不等於t,它們不是同一個對象"); } } }
運行結果:
• s等於t,它們是同一個對象
這又是爲何呢?緣由是這樣的,對於常量來說,它的值是固定的,所以在編譯期就能被肯定了,而變量的值只有到運行時才能被肯定,由於這個變量能夠被不一樣的方法調用,從而可能引發值的改變。在上面的例子中,A和B都是常量,值是固定的,所以s的值也是固定的,它在類被編譯時就已經肯定了。也就是說:
String s=A+B;
等價於
String s="ab"+"cd";
我對上面的例子稍加改變看看會出現什麼狀況
public class StringStaticTest { // 常量A public static final String A; // 常量B public static final String B; static { A = "ab"; B = "cd"; } public static void main(String[] args) { // 將兩個常量用+鏈接對s進行初始化 String s = A + B; String t = "abcd"; if (s == t) { System.out.println("s等於t,它們是同一個對象"); } else { System.out.println("s不等於t,它們不是同一個對象"); } } }
它的運行結果是這樣:
只是作了一點改動,結果就和剛剛的例子剛好相反。咱們再來分析一下。A和B雖然被定義爲常量(只能被賦值一次),可是它們都沒有立刻被賦值。在運算出s的值以前,他們什麼時候被賦值,以及被賦予什麼樣的值,都是個變數。所以A和B在被賦值以前,性質相似於一個變量。那麼s就不能在編譯期被肯定,而只能在運行時被建立了。
因爲字符串池中對象的共享可以帶來效率的提升,所以咱們提倡你們用引號包含文本的方式來建立String對象,實際上這也是咱們在編程中常採用的。
接下來咱們再來看看intern()方法,它的定義以下:
1. public native String intern();
這是一個本地方法。在調用這個方法時,JAVA虛擬機首先檢查字符串池中是否已經存在與該對象值相等對象存在,若是有則返回字符串池中對象的引用;若是沒有,則先在字符串池中建立一個相同值的String對象,而後再將它的引用返回。
咱們來看這段代碼:
public class StringInternTest { public static void main(String[] args) { // 使用char數組來初始化a,避免在a被建立以前字符串池中已經存在了值爲"abcd"的對象 String a = new String(new char[]{'a', 'b', 'c', 'd'}); String b = a.intern(); if (b == a) { System.out.println("b被加入了字符串池中,沒有新建對象"); } else { System.out.println("b沒被加入字符串池中,新建了對象"); } } }
運行結果:
若是String類的intern()方法在沒有找到相同值的對象時,是把當前對象加入字符串池中,而後返回它的引用的話,那麼b和a指向的就是同一個對象;不然b指向的對象就是JAVA虛擬機在字符串池中新建的,只是它的值與a相同罷了。上面這段代碼的運行結果偏偏印證了這一點。
最後咱們再來講說String對象在JAVA虛擬機(JVM)中的存儲,以及字符串池與堆(heap)和棧(stack)的關係。咱們首先回顧一下堆和棧的區別:
• 棧(stack):主要保存基本類型(或者叫內置類型)(char、byte、short、int、long、float、double、boolean)和對象的引用,數據能夠共享,速度僅次於寄存器(register),快於堆。
• 堆(heap):用於存儲對象。
咱們查看String類的源碼就會發現,它有一個value屬性,保存着String對象的值,類型是char[],這也正說明了字符串就是字符的序列。
當執行String a="abc";時,JAVA虛擬機會在棧中建立三個char型的值'a'、'b'和'c',而後在堆中建立一個String對象,它的值(value)是剛纔在棧中建立的三個char型值組成的數組{'a','b','c'},最後這個新建立的String對象會被添加到字符串池中。若是咱們接着執行String b=new String("abc");代碼,因爲"abc"已經被建立並保存於字符串池中,所以JAVA虛擬機只會在堆中新建立一個String對象,可是它的值(value)是共享前一行代碼執行時在棧中建立的三個char型值值'a'、'b'和'c'。
說到這裏,咱們對於篇首提出的String str=new String("abc")爲何是建立了兩個對象這個問題就已經至關明瞭了。