一、創建。
好了,知道String是非可變類以後,我們可以進一步瞭解String的構造方式了。創建一個Stirng對象,主要就有以下兩種方式:
- String str1 = new String("abc");
- Stirng str2 = "abc";
雖然兩個語句都是返回一個String對象的引用,但是jvm對兩者的處理方式是不一樣的。對於第一種,jvm會馬上在heap中創建一個String對 象,然後將該對象的引用返回給用戶。對於第二種,jvm首先會在內部維護的strings pool中通過String的 equels 方法查找是對象池中是否存放有該String對象,如果有,則返回已有的String對象給用戶,而不會在heap中重新創建一個新的String對象; 如果對象池中沒有該String對象,jvm則在heap中創建新的String對象,將其引用返回給用戶,同時將該引用添加至strings pool中。注意:使用第一種方法創建對象時,jvm是不會主動把該對象放到strings pool裡面的,除非程序調用 String的intern方法。看下面的例子:
- String str1 = new String("abc"); //jvm 在堆上創建一個String對象
-
- //jvm 在strings pool中找不到值為「abc」的字符串,因此
- //在堆上創建一個String對象,並將該對象的引用加入至strings pool中
- //此時堆上有兩個String對象
- Stirng str2 = "abc";
-
- if(str1 == str2){
- System.out.println("str1 == str2");
- }else{
- System.out.println("str1 != str2");
- }
- //打印結果是 str1 != str2,因為它們是堆上兩個不同的對象
-
- String str3 = "abc";
- //此時,jvm發現strings pool中已有「abc」對象了,因為「abc」equels 「abc」
- //因此直接返回str2指向的對象給str3,也就是說str2和str3是指向同一個對象的引用
- if(str2 == str3){
- System.out.println("str2 == str3");
- }else{
- System.out.println("str2 != str3");
- }
- //打印結果為 str2 == str3
再看下面的例子:
- String str1 = new String("abc"); //jvm 在堆上創建一個String對象
-
- str1 = str1.intern();
- //程序顯式將str1放到strings pool中,intern運行過程是這樣的:首先查看strings pool
- //有沒「abc」對象的引用,沒有,則在堆中新建一個對象,然後將新對象的引用加入至
- //strings pool中。執行完該語句後,str1原來指向的String對象已經成為垃圾對象了,隨時會
- //被GC收集。
-
- //此時,jvm發現strings pool中已有「abc」對象了,因為「abc」equels 「abc」
- //因此直接返回str1指向的對象給str2,也就是說str2和str1引用著同一個對象,
- //此時,堆上的有效對象只有一個。
- Stirng str2 = "abc";
-
- if(str1 == str2){
- System.out.println("str1 == str2");
- }else{
- System.out.println("str1 != str2");
- }
- //打印結果是 str1 == str2
-
為什麼jvm可以這樣處理String對象呢?就是因為String的非可變性。既然所引用的對象一旦創建就永不更改,那麼多個引用共用一個對象時互不影響。
二、串接(Concatenation)。
java程序員應該都知道濫用String的串接操作符是會影響程序的性能的。性能問題從何而來呢?歸根結底就是String類的非可變性。既然 String對象都是非可變的,也就是對象一旦創建了就不能夠改變其內在狀態了,但是串接操作明顯是要增長字符串的,也就是要改變String的內部狀 態,兩者出現了矛盾。怎麼辦呢?要維護String的非可變性,只好在串接完成後新建一個String 對象來表示新產生的字符串了。也就是說,每一次執行串接操作都會導致新對象的產生,如果串接操作執行很頻繁,就會導致大量對象的創建,性能問題也就隨之而 來了。
為瞭解決這個問題,jdk為String類提供了一個可變的配套類,StringBuffer。使用StringBuffer對象,由於該類是可變的,串 接時僅僅時改變了內部數據結構,而不會創建新的對象,因此性能上有很大的提高。針對單線程,jdk 5.0還提供了StringBuilder類,在單線程環境下,由於不用考慮同步問題,使用該類使性能得到進一步的提高。
三、String的長度
我們可以使用串接操作符得到一個長度更長的字符串,那麼,String對象最多能容納多少字符呢?查看String的源代碼我們可以得知類String中 是使用域 count 來記錄對象字符的數量,而count 的類型為 int,因此,我們可以推測最長的長度為 2^32,也就是4G。
不過,我們在編寫源代碼的時候,如果使用 Sting str = "aaaa";的形式定義一個字符串,那麼雙引號裡面的ASCII字符最多只能有 65534 個。為什麼呢?因為在class文件的規範中, CONSTANT_Utf8_info表中使用一個16位的無符號整數來記錄字符串的長度的,最多能表示 65536個字節,而java class 文件是使用一種變體UTF-8格式來存放字符的,null值使用兩個字節來表示,因此只剩下 65536- 2 = 65534個字節。也正是變體UTF-8的原因,如果字符串中含有中文等非ASCII字符,那麼雙引號中字符的數量會更少(一個中文字符佔用三個字節)。 如果超出這個數量,在編譯的時候編譯器會報錯。
=============================================================
String a="Hello World!";
String b="Hello World!";
a==b? a和b是否相等 ? 為什麼?
String a=new String("Hello World!");
String b="Hello World!";
a==b? a和b是否相等 ? 為什麼?
解釋:
1. 首先String不屬於8種基本數據類型,String是一個對象。
因為對象的默認值是null,所以String的默認值也是null;但它又是一種特殊的對象,有其它對象沒有的一些特性。
2. new String()和new String(「」)都是申明一個新的空字符串,是空串不是null;
3. String str="kvill";和String str=new String (「kvill」);的區別:
在這裡,我們不談堆,也不談棧,只先簡單引入常量池這個簡單的概念。
常量池(constant pool)指的是在編譯期被確定,並被保存在已編譯的.class文件中的
一些數據。它包括了關於類、方法、接口等中的常量,也包括字符串常量。
看例1:
String s0="kvill";
String s1="kvill";
String s2="kv" + "ill";
System.out.println( s0==s1 );
System.out.println( s0==s2 );
結果為:
true
true
首先,我們要知道Java會確保一個字符串常量只有一個拷貝。
因為例子中的s0和s1中的」kvill」都是字符串常量,它們在編譯期就被確定了,所以
s0==s1為true;而」kv」和」ill」也都是字符串常量,當一個字符串由多個字符串常量連
接而成時,它自己肯定也是字符串常量,所以s2也同樣在編譯期就被解析為一個字符串常量,所以s2也是常量池中」kvill」的一個引用。所以我們得出s0==s1==s2;
用new String() 創建的字符串不是常量,不能在編譯期就確定,所以new String()
創建的字符串不放入常量池中,它們有自己的地址空間。
看例2:
String s0="kvill";
String s1=new String("kvill");
String s2="kv"+ new String("ill");
System.out.println( s0==s1 );
System.out.println( s0==s2 );
System.out.println( s1==s2 );
結果為:
false
false
false
例2中s0還是常量池中」kvill」的應用,s1因為無法在編譯期確定,所以是運行時創
建的新對象」kvill」的引用,s2因為有後半部分new String(「ill」)所以也無法在編譯
期確定,所以也是一個新創建對象」kvill」的應用;明白了這些也就知道為何得出此結果了。
4. String.intern():
再補充介紹一點:存在於.class文件中的常量池,在運行期被JVM裝載,並且可以擴充。String的intern()方法就是擴充常量池的 一個方法;當一個String實例str調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引 用,如果沒有,則在常量池中增加一個Unicode等於str的字符串並返回它的引用;看例3就清楚了。
例3:
String s0= "kvill";
String s1=new String("kvill");
String s2=new String("kvill");
System.out.println( s0==s1 );
System.out.println( 「**********」 );
s1.intern();
s2=s2.intern(); //把常量池中"kvill"的引用賦給s2
System.out.println( s0==s1);
System.out.println( s0==s1.intern() );
System.out.println( s0==s2 );
結果為:
false
**********
false //雖然執行了s1.intern(),但它的返回值沒有賦給s1
true //說明s1.intern()返回的是常量池中」kvill」的引用
true
最後我再破除一個錯誤的理解:
有人說,「使用String.intern()方法則可以將一個String類的保存到一個全局Strin
g表中,如果具有相同值的Unicode字符串已經在這個表中,那麼該方法返回表中已有字符串的地址,如果在表中沒有相同值的字符串,則將自己的 地址註冊到表中「如果我把他說的這個全局的String表理解為常量池的話,他的最後一句話,「如果在表中沒有相同值的字符串,則將自己的地址註冊到表 中」是錯的:
看例4:
String s1=new String("kvill");
String s2=s1.intern();
System.out.println( s1==s1.intern() );
System.out.println( s1+" "+s2 );
System.out.println( s2==s1.intern() );
結果:
false
kvill kvill
true
在這個類中我們沒有聲名一個"kvill"常量,所以s1.intern()同new String("kvill")是不同的,當我們調用s1.intern()後就在常量池中新添加了一個"kvill"常量,原來的不在常量池中 的"kvill"仍然存在,也就不是「將自己的地址註冊到常量池中」了。
s1==s1.intern()為false說明原來的「kvill」仍然存在;
s2現在為常量池中「kvill」的地址,所以有s2==s1.intern()為true。
5. 關於equals()和==:
這個對於String簡單來說就是比較兩字符串的Unicode序列是否相當,如果相等返回true;而==是比較兩字符串的地址是否相同,也就是是否是同一個字符串的引用。
6. 關於String是不可變的
這一說又要說很多,大家只要知道String的實例一旦生成就不會再改變了,比如說:
String str=」kv」+」ill」+」 「+」ans」;
就是有4個字符串常量,首先」kv」和」ill」生成了」kvill」存在內存中,然後」kvill」又和」 「 生成 」kvill 「存在內存中,最後又和生成了」kvill ans」;並把這個字符串的地址賦給了str,就是因為String的「不可變」產生了很多臨時變量,這也就是為什麼建議用StringBuffer的原 因了,因為StringBuffer是可改變的。
7.注意
看下面例子:
public class StringTest
{
public static void main(String[] args)
{
String str1 = "hello";
String str2 = "hel";
str2 = str2 + "lo";
System.out.println("str1 == str2 :" + (str1 == str2));
}
}
實際會打出false
為什麼呢,關鍵就在於str2=str2+"lo"是不能能編譯期就確定的
str1是在內存池沒錯,但str2不是~
用反編譯工具反編譯一下class文件就會發現
str2 =str2+"lo";
實際上是:
str2 = (new StringBuilder()).append(str2).append("lo").toString();
顯然,str2是new出來的(不信去看看StringBuilder的源代碼)
沒有留言:
張貼留言