1.
棧
(stack)
與堆
(heap)
都是
Java
用來在
Ram
中存放數據的地方。與
C++
不同,
Java
自動管理棧和堆,程序員不能直接地設置?;蚨?。
2. 棧的優勢是,存取速度比堆要快,僅次于直接位于 CPU 中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據可以共享,詳見第 3 點。堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器, Java 的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由于要在運行時動態分配內存,存取速度較慢。
3. Java 中的數據類型有兩種。
一種是基本類型 (primitive types), 共有 8 種,即 int, short, long, byte, float, double, boolean, char( 注意,并沒有 string 的基本類型 ) 。這種類型的定義是通過諸如 int a = 3; long b =255L ;
的形式來定義的,稱為自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這里并沒有類的存在。如
int a = 3;
這里的
a
是一個指向
int
類型的引用,指向
3
這個字面值。這些字面值的數據,由于大小可知,生存期可知
(
這些字面值固定定義在某個程序塊里面,程序塊退出后,字段值就消失了
)
,出于追求速度的原因,就存在于棧中。
另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義
int a = 3;?
int b = 3 ;
編譯器先處理 int a = 3 ;首先它會在棧中創建一個變量為 a 的引用,然后查找有沒有字面值為 3 的地址,沒找到,就開辟一個存放 3 這個字面值的地址,然后將 a 指向 3 的地址。接著處理 int b = 3 ;在創建完 b 的引用變量后,由于在棧中已經有 3 這個字面值,便將 b 直接指向 3 的地址。這樣,就出現了 a 與 b 同時均指向 3 的情況。
特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那么另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完 a 與 b 的值后,再令 a=4 ;那么, b 不會等于 4 ,還是等于 3 。在編譯器內部,遇到 a=4 ;時,它就會重新搜索棧中是否有 4 的字面值,如果沒有,重新開辟地址存放 4 的值;如果已經有了,則直接將 a 指向這個地址。因此 a 值的改變不會影響到 b 的值。
另一種是包裝類數據,如 Integer, String, Double 等將相應的基本數據類型包裝起來的類。這些類數據全部存在于堆中, Java 用 new() 語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要占用更多的時間。
4. String 是一個特殊的包裝類數據。即可以用 String str = new String("abc"); 的形式來創建,也可以用 String str = "abc" ;的形式來創建 ( 作為對比,在 JDK 5.0 之前,你從未見過 Integer i = 3; 的表達式,因為類與字面值是不能通用的,除了 String 。而在 JDK 5.0 中,這種表達式是可以的!因為編譯器在后臺進行 Integer i = new Integer(3) 的轉換 ) 。前者是規范的類的創建過程,即在 Java 中,一切都是對象,而對象是類的實例,全部通過 new() 的形式來創建。 Java 中的有些類,如 DateFormat 類,可以通過該類的 getInstance() 方法來返回一個新創建的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部通過 new() 來創建的,而 getInstance() 向外部隱藏了此細節。那為什么在 String str = "abc" ;中,并沒有通過 new() 來創建實例,是不是違反了上述原則?其實沒有。
5. 關于 String str = "abc" 的內部工作。 Java 內部將此語句轉化為以下幾個步驟:
(1) 先定義一個名為 str 的對 String 類的對象引用變量: String str ;
(2) 在棧中查找有沒有存放值為 "abc" 的地址,如果沒有,則開辟一個存放字面值為 "abc" 的地址,接著創建一個新的 String 類的對象 o ,并將 o 的字符串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的對象 o 。如果已經有了值為 "abc" 的地址,則查找對象 o ,并返回 o 的地址。
(3) 將 str 指向對象 o 的地址。
值得注意的是,一般 String 類中字符串值都是直接存值的。但像 String str = "abc" ;這種場合下,其字符串值卻是保存了一個指向存在棧中數據的引用!
??
為了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2);??//true
??
注意,我們這里并不用 str1.equals(str2) ;的方式,因為這將比較兩個字符串的值是否相等。 == 號,根據 JDK 的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這里要看的是, str1 與 str2 是否都指向了同一個對象。
結果說明, JVM 創建了兩個引用 str1 和 str2 ,但只創建了一個對象,而且兩個引用都指向了這個對象。
我們再來更進一步,將以上代碼改成:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
System.out.println(str1 + "," + str2);??//bcd, abc
System.out.println(str1==str2);??//false
這就是說,賦值的變化導致了類對象引用的變化, str1 指向了另外一個新對象!而 str2 仍舊指向原來的對象。上例中,當我們將 str1 的值改為 "bcd" 時, JVM 發現在棧中沒有存放該值的地址,便開辟了這個地址,并創建了一個新的對象,其字符串的值指向這個地址。
事實上, String 類被設計成為不可改變 (immutable) 的類。如果你要改變其值,可以,但 JVM 在運行時根據新值悄悄創建了一個新對象,然后將這個對象的地址返回給原來類的引用。這個創建過程雖說是完全自動進行的,但它畢竟占用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
再修改原來代碼:
String str1 = "abc";
String str2 = "abc";
??
str1 = "bcd";
??
String str3 = str1;
System.out.println(str3);??//bcd
String str4 = "bcd";
System.out.println(str1 == str4);??//true
????
str3 這個對象的引用直接指向 str1 所指向的對象 ( 注意, str3 并沒有創建新對象 ) 。當 str1 改完其值后,再創建一個 String 的引用 str4 ,并指向因 str1 修改值而創建的新的對象??梢园l現,這回 str4 也沒有創建新的對象,從而再次實現棧中數據的共享。
我們再接著看以下的代碼。
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2);??//false
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2);??//false
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
以上兩段代碼說明,只要是用 new() 來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。
6. 數據類型包裝類的值不可修改。不僅僅是 String 類的值不可修改,所有的數據類型包裝類都不能更改其內部的值。
7. 結論與建議:
(1) 我們在使用諸如 String str = "abc" ;的格式定義類時,總是想當然地認為,我們創建了 String 類的對象 str 。擔心陷阱!對象可能并沒有被創建!唯一可以肯定的是,指向 String 類的引用被創建了。至于這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,除非你通過 new() 方法來顯要地創建一個新的對象。因此,更為準確的說法是,我們創建了一個指向 String 類的對象的引用變量 str ,這個對象引用變量指向了某個值為 "abc" 的 String 類。清醒地認識到這一點對排除程序中難以發現的 bug 是很有幫助的。
(2) 使用 String str = "abc" ;的方式,可以在一定程度上提高程序的運行速度,因為 JVM 會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對于 String str = new String("abc") ;的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。這個思想應該是享元模式的思想,但 JDK 的內部在這里實現是否應用了這個模式,不得而知。
(3) 當比較包裝類里面的數值是否相等時,用 equals() 方法;當測試兩個包裝類的引用是否指向同一個對象時,用 == 。
(4) 由于 String 類的 immutable 性質,當 String 變量需要經常變換其值時,應該考慮使用 StringBuffer 類,以提高程序效率。
2. 棧的優勢是,存取速度比堆要快,僅次于直接位于 CPU 中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據可以共享,詳見第 3 點。堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器, Java 的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由于要在運行時動態分配內存,存取速度較慢。
3. Java 中的數據類型有兩種。
一種是基本類型 (primitive types), 共有 8 種,即 int, short, long, byte, float, double, boolean, char( 注意,并沒有 string 的基本類型 ) 。這種類型的定義是通過諸如 int a = 3; long b =
另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義
int a = 3;?
int b = 3 ;
編譯器先處理 int a = 3 ;首先它會在棧中創建一個變量為 a 的引用,然后查找有沒有字面值為 3 的地址,沒找到,就開辟一個存放 3 這個字面值的地址,然后將 a 指向 3 的地址。接著處理 int b = 3 ;在創建完 b 的引用變量后,由于在棧中已經有 3 這個字面值,便將 b 直接指向 3 的地址。這樣,就出現了 a 與 b 同時均指向 3 的情況。
特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那么另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完 a 與 b 的值后,再令 a=4 ;那么, b 不會等于 4 ,還是等于 3 。在編譯器內部,遇到 a=4 ;時,它就會重新搜索棧中是否有 4 的字面值,如果沒有,重新開辟地址存放 4 的值;如果已經有了,則直接將 a 指向這個地址。因此 a 值的改變不會影響到 b 的值。
另一種是包裝類數據,如 Integer, String, Double 等將相應的基本數據類型包裝起來的類。這些類數據全部存在于堆中, Java 用 new() 語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要占用更多的時間。
4. String 是一個特殊的包裝類數據。即可以用 String str = new String("abc"); 的形式來創建,也可以用 String str = "abc" ;的形式來創建 ( 作為對比,在 JDK 5.0 之前,你從未見過 Integer i = 3; 的表達式,因為類與字面值是不能通用的,除了 String 。而在 JDK 5.0 中,這種表達式是可以的!因為編譯器在后臺進行 Integer i = new Integer(3) 的轉換 ) 。前者是規范的類的創建過程,即在 Java 中,一切都是對象,而對象是類的實例,全部通過 new() 的形式來創建。 Java 中的有些類,如 DateFormat 類,可以通過該類的 getInstance() 方法來返回一個新創建的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部通過 new() 來創建的,而 getInstance() 向外部隱藏了此細節。那為什么在 String str = "abc" ;中,并沒有通過 new() 來創建實例,是不是違反了上述原則?其實沒有。
5. 關于 String str = "abc" 的內部工作。 Java 內部將此語句轉化為以下幾個步驟:
(1) 先定義一個名為 str 的對 String 類的對象引用變量: String str ;
(2) 在棧中查找有沒有存放值為 "abc" 的地址,如果沒有,則開辟一個存放字面值為 "abc" 的地址,接著創建一個新的 String 類的對象 o ,并將 o 的字符串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的對象 o 。如果已經有了值為 "abc" 的地址,則查找對象 o ,并返回 o 的地址。
(3) 將 str 指向對象 o 的地址。
值得注意的是,一般 String 類中字符串值都是直接存值的。但像 String str = "abc" ;這種場合下,其字符串值卻是保存了一個指向存在棧中數據的引用!
??
為了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2);??//true
??
注意,我們這里并不用 str1.equals(str2) ;的方式,因為這將比較兩個字符串的值是否相等。 == 號,根據 JDK 的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這里要看的是, str1 與 str2 是否都指向了同一個對象。
結果說明, JVM 創建了兩個引用 str1 和 str2 ,但只創建了一個對象,而且兩個引用都指向了這個對象。
我們再來更進一步,將以上代碼改成:
String str1 = "abc";
String str2 = "abc";
str1 = "bcd";
System.out.println(str1 + "," + str2);??//bcd, abc
System.out.println(str1==str2);??//false
這就是說,賦值的變化導致了類對象引用的變化, str1 指向了另外一個新對象!而 str2 仍舊指向原來的對象。上例中,當我們將 str1 的值改為 "bcd" 時, JVM 發現在棧中沒有存放該值的地址,便開辟了這個地址,并創建了一個新的對象,其字符串的值指向這個地址。
事實上, String 類被設計成為不可改變 (immutable) 的類。如果你要改變其值,可以,但 JVM 在運行時根據新值悄悄創建了一個新對象,然后將這個對象的地址返回給原來類的引用。這個創建過程雖說是完全自動進行的,但它畢竟占用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
再修改原來代碼:
String str1 = "abc";
String str2 = "abc";
??
str1 = "bcd";
??
String str3 = str1;
System.out.println(str3);??//bcd
String str4 = "bcd";
System.out.println(str1 == str4);??//true
????
str3 這個對象的引用直接指向 str1 所指向的對象 ( 注意, str3 并沒有創建新對象 ) 。當 str1 改完其值后,再創建一個 String 的引用 str4 ,并指向因 str1 修改值而創建的新的對象??梢园l現,這回 str4 也沒有創建新的對象,從而再次實現棧中數據的共享。
我們再接著看以下的代碼。
String str1 = new String("abc");
String str2 = "abc";
System.out.println(str1==str2);??//false
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
String str1 = "abc";
String str2 = new String("abc");
System.out.println(str1==str2);??//false
創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
以上兩段代碼說明,只要是用 new() 來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。
6. 數據類型包裝類的值不可修改。不僅僅是 String 類的值不可修改,所有的數據類型包裝類都不能更改其內部的值。
7. 結論與建議:
(1) 我們在使用諸如 String str = "abc" ;的格式定義類時,總是想當然地認為,我們創建了 String 類的對象 str 。擔心陷阱!對象可能并沒有被創建!唯一可以肯定的是,指向 String 類的引用被創建了。至于這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,除非你通過 new() 方法來顯要地創建一個新的對象。因此,更為準確的說法是,我們創建了一個指向 String 類的對象的引用變量 str ,這個對象引用變量指向了某個值為 "abc" 的 String 類。清醒地認識到這一點對排除程序中難以發現的 bug 是很有幫助的。
(2) 使用 String str = "abc" ;的方式,可以在一定程度上提高程序的運行速度,因為 JVM 會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對于 String str = new String("abc") ;的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。這個思想應該是享元模式的思想,但 JDK 的內部在這里實現是否應用了這個模式,不得而知。
(3) 當比較包裝類里面的數值是否相等時,用 equals() 方法;當測試兩個包裝類的引用是否指向同一個對象時,用 == 。
(4) 由于 String 類的 immutable 性質,當 String 變量需要經常變換其值時,應該考慮使用 StringBuffer 類,以提高程序效率。