java中String和char(面試必考題)
一 char和string的區別:
1 char是表示的是字元,定義的時候用單引號,只能儲存一個字元。例如; char='d'.
而String表示的是字串,定義的時候用雙引號,可以儲存一個或者多個字元。例如:String=“we are neuer”。
2 char是基本資料型別,而String是個類,屬於引用資料型別。String類可以呼叫方法,具有面向物件的特徵。
二 char型別
char在Java中是16位的,因為Java用的是Unicode。
三 String型別
1 java設計了兩種不同的方法來生成字串物件,一種是使用雙引號,一種是呼叫String類的建構函式。
String str1 = "we are student";
String str2 = new String ("qau neu");
注:Java為String型別提供了緩衝池機制,當使用雙引號定義物件時,Java環境首先去字串緩衝池尋找內容相同的字串,如果存在就拿出來使用,否則就建立一個新的字串放在緩衝池中。
例如: String S=new String("abc''), 產生(或者建立)幾個物件?
答案是:產生一個或者兩個物件。如果常量池中原來沒有“abc",就產兩個物件,如果字串常量池中"abc",就產生一個物件。
因此,這個問題如果換成 String str = new String("abc")涉及到幾個String物件?合理的解釋是2個。
1)String類是final類,也即意味著String類不能被繼承,並且它的成員方法都預設為final方法。在Java中,被final修飾的類是不允許被繼承的,並且該類中的成員方法都預設為final方法。
在這裡要永遠記住一點:“String物件一旦被建立就是固定不變的了,對String物件的任何改變都不影響到原物件,相關的任何change操作都會生成新的物件”。
2 字串常量池
我們知道字串的分配和其他物件分配一樣,是需要消耗高昂的時間和空間的,而且字串我們使用的非常多。JVM為了提高效能和減少記憶體的開銷,在例項化字串的時候進行了一些優化:使用字串常量池。每當我們建立字串常量時,JVM會首先檢查字串常量池,如果該字串已經存在常量池中,那麼就直接返回常量池中的例項引用。如果字串不存在常量池中,就會例項化該字串並且將其放到常量池中。由於String字串的不可變性我們可以十分肯定常量池中一定不存在兩個相同的字串
Java中的常量池,實際上分為兩種形態:靜態常量池和執行時常量池。
所謂靜態常量池,即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字串(數字)字面量,還包含類、方法的資訊,佔用class檔案絕大部分空間。
而執行時常量池,則是jvm虛擬機器在完成類裝載操作後,將class檔案中的常量池載入到記憶體中,並儲存在方法區中,我們常說的常量池,就是指方法區中的執行時常量池。
例如下面的例子:
總結:雖然a、b、c、chenssy是不同的物件,但是從String的內部結構我們是可以理解上面的。String c = new String("chenssy");雖然c的內容是建立在堆中,但是他的內部value還是指向JVM常量池的chenssy的value,它構造chenssy時所用的引數依然是chenssy字串常量。
例1
public viod test{
String str1="aaa";
String str2="aaa";
System.out.println(str1==str2);
}
輸出的結果: True
分析:當執行String str1="aaa"時,JVM首先會去字串池中查詢是否存在"aaa"這個物件,如果不存在,則在字串池中建立"aaa"這個物件,然後將池中"aaa"這個物件的引用地址返回給字串常量str1,這樣str1會指向池中"aaa"這個字串物件;如果存在,則不建立任何物件,直接將池中"aaa"這個物件的地址返回,賦給字串常量。當建立字串物件str2時,字串池中已經存在"aaa"這個物件,直接把物件"aaa"的引用地址返回給str2,這樣str2指向了池中"aaa"這個物件,也就是說str1和str2指向了同一個物件,因此語句System.out.println(str1 == str2)輸出:true。
例2
public void test2(){
String str3=new String("aaa");
String str4=new String("aaa");
System.out.println("===========test2============");
System.out.println(str3==str4);//false 可以看出用new的方式是生成不同的物件
}
輸出的結果為:false
分析: 採用new關鍵字新建一個字串物件時,JVM首先在字串池中查詢有沒有"aaa"這個字串物件,如果有,則不在池中再去建立"aaa"這個物件了,直接在堆中建立一個"aaa"字串物件,然後將堆中的這個"aaa"物件的地址返回賦給引用str3,這樣,str3就指向了堆中建立的這個"aaa"字串物件;如果沒有,則首先在字串池中建立一個"aaa"字串物件,然後再在堆中建立一個"aaa"字串物件,然後將堆中這個"aaa"字串物件的地址返回賦給str3引用,這樣,str3指向了堆中建立的這個"aaa"字串物件。當執行String str4=new String("aaa")時, 因為採用new關鍵字建立物件時,每次new出來的都是一個新的物件,也即是說引用str3和str4指向的是兩個不同的物件,因此語句System.out.println(str3 == str4)輸出:false。
例 3
/**
* 編譯期確定
*/
public void test3(){
String s0="helloworld";
String s1="helloworld";
String s2="hello"+"world";
System.out.println("===========test3============");
System.out.println(s0==s1); //true 可以看出s0跟s1是指向同一個物件
System.out.println(s0==s2); //true 可以看出s0跟s2是指向同一個物件
}
輸出結果:True,True。
分析:因為例子中的s0和s1中的"helloworld”都是字串常量,它們在編譯期就被確定了,所以s0==s1為true;而"hello”和"world”也都是字串常量,當一個字串由多個字串常量連線而成時,它自己肯定也是字串常量,所以s2也同樣在編譯期就被解析為一個字串常量,所以s2也是常量池中"helloworld”的一個引用。所以我們得出s0==s1==s2。
例 4
/**
* 編譯期無法確定
*/
public void test4(){
String s0="helloworld";
String s1=new String("helloworld");
String s2="hello" + new String("world");
System.out.println("===========test4============");
System.out.println( s0==s1 ); //false
System.out.println( s0==s2 ); //false
System.out.println( s1==s2 ); //false
}
答案:false,false,false。
分析:用new String() 建立的字串不是常量,不能在編譯期就確定,所以new String() 建立的字串不放入常量池中,它們有自己的地址空間
s0還是常量池中"helloworld”的引用,s1因為無法在編譯期確定,所以是執行時建立的新物件"helloworld”的引用,s2因為有後半部分new String(”world”)所以也無法在編譯期確定,所以也是一個新建立物件"helloworld”的引用。
例 5:
/*
* 編譯期無法確定
*/
public void test7(){
String s0 = "ab";
String s1 = "b";
String s2 = "a" + s1;
System.out.println("===========test7============");
System.out.println((s0 == s2)); //result = false
}
輸出的結果:false。
分析:JVM對於字串引用,由於在字串的"+"連線中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即"a" + s1無法被編譯器優化,只有在程式執行期來動態分配並將連線後的新地址賦給s2。所以上面程式的結果也就為false。
例 6:
**
* 繼續-編譯期無法確定
*/
public void test5(){
String str1="abc";
String str2="def";
String str3=str1+str2;
System.out.println("===========test5============");
System.out.println(str3=="abcdef"); //false
輸出結果: false。
分析:因為str3指向堆中的"abcdef"物件,而"abcdef"是字串池中的物件,所以結果為false。JVM對String str="abc"物件放在常量池中是在編譯時做的,而String str3=str1+str2是在執行時刻才能知道的。new物件也是在執行時才做的。而這段程式碼總共建立了5個物件,字串池中兩個、堆中三個。+運算子會在堆中建立來兩個String物件,這兩個物件的值分別是"abc"和"def",也就是說從字串池中複製這兩個值,然後在堆中建立兩個物件,然後再建立物件str3,然後將"abcdef"的堆地址賦給str3。
步驟:
1)棧中開闢一塊中間存放引用str1,str1指向池中String常量"abc"。
2)棧中開闢一塊中間存放引用str2,str2指向池中String常量"def"。
3)棧中開闢一塊中間存放引用str3。
4)str1 + str2通過StringBuilder的最後一步toString()方法還原一個新的String物件"abcdef",因此堆中開闢一塊空間存放此物件。
5)引用str3指向堆中(str1 + str2)所還原的新String物件。
6)str3指向的物件在堆中,而常量"abcdef"在池中,輸出為false。
例 7;
public void test7(){
String s0 = "ab";
String s1 = "b";
String s2 = "a" + s1;
System.out.println("===========test7============");
System.out.println((s0 == s2)); //result = false
}
輸出的結果:false。
分析:JVM對於字串引用,由於在字串的"+"連線中,有字串引用存在,而引用的值在程式編譯期是無法確定的,即"a" + s1無法被編譯器優化,只有在程式執行期來動態分配並將連線後的新地址賦給s2。所以上面程式的結果也就為false。
例 8
/**
* 比較字串常量的“+”和字串引用的“+”的區別
*/
public void test8(){
String test="javalanguagespecification";
String str="java";
String str1="language";
String str2="specification";
System.out.println("===========test8============");
System.out.println(test == "java" + "language" + "specification");
System.out.println(test == str + str1 + str2);
}
執行上述程式碼,結果為:true、false。
分析:為什麼出現上面的結果呢?這是因為,字串字面量拼接操作是在Java編譯器編譯期間就執行了,也就是說編譯器編譯時,直接把"java"、"language"和"specification"這三個字面量進行"+"操作得到一個"javalanguagespecification" 常量,並且直接將這個常量放入字串池中,這樣做實際上是一種優化,將3個字面量合成一個,避免了建立多餘的字串物件。而字串引用的"+"運算是在Java執行期間執行的,即str + str2 + str3在程式執行期間才會進行計算,它會在堆記憶體中重新建立一個拼接後的字串物件。總結來說就是:字面量"+"拼接是在編譯期間進行的,拼接後的字串存放在字串池中;而字串引用的"+"拼接運算實在執行時進行的,新建立的字串存放在堆中。
對於直接相加字串,效率很高,因為在編譯器便確定了它的值,也就是說形如"I"+"love"+"java"; 的字串相加,在編譯期間便被優化成了"Ilovejava"。對於間接相加(即包含字串引用),形如s1+s2+s3; 效率要比直接相加低,因為在編譯器不會對引用變數進行優化。
例 9
public void test9(){
String s0 = "ab";
final String s1 = "b";
String s2 = "a" + s1;
System.out.println("===========test9============");
System.out.println((s0 == s2)); //result = true
}
輸出結果:true。
分析:和例子7中唯一不同的是s1字串加了final修飾,對於final修飾的變數,它在編譯時被解析為常量值的一個本地拷貝儲存到自己的常量池中或嵌入到它的位元組碼流中。所以此時的"a" + s1和"a" + "b"效果是一樣的。故上面程式的結果為true。
例 10
/**
* 編譯期無法確定
*/
public void test10(){
String s0 = "ab";
final String s1 = getS1();
String s2 = "a" + s1;
System.out.println("===========test10============");
System.out.println((s0 == s2)); //result = false
}
private static String getS1() {
return "b";
}
執行上述程式碼,結果為:false。
分析:這裡面雖然將s1用final修飾了,但是由於其賦值是通過方法呼叫返回的,那麼它的值只能在執行期間確定,因此s0和s2指向的不是同一個物件,故上面程式的結果為false。
三、總結
1.String類初始化後是不可變的(immutable)
String使用private final char value[]來實現字串的儲存,也就是說String物件建立之後,就不能再修改此物件中儲存的字串內容,就是因為如此,才說String型別是不可變的(immutable)。程式設計師不能對已有的不可變物件進行修改。我們自己也可以建立不可變物件,只要在介面中不提供修改資料的方法就可以。
然而,String類物件確實有編輯字串的功能,比如replace()。這些編輯功能是通過建立一個新的物件來實現的,而不是對原有物件進行修改。比如:
s = s.replace("World", "Universe");
上面對s.replace()的呼叫將建立一個新的字串"Hello Universe!",並返回該物件的引用。通過賦值,引用s將指向該新的字串。如果沒有其他引用指向原有字串"Hello World!",原字串物件將被垃圾回收。
2.引用變數與物件
A aa;
這個語句宣告一個類A的引用變數aa[我們常常稱之為控制代碼],而物件一般通過new建立。所以aa僅僅是一個引用變數,它不是物件。
3.建立字串的方式
建立字串的方式歸納起來有兩類:
(1)使用""引號建立字串;
(2)使用new關鍵字建立字串。
結合上面例子,總結如下:
(1)單獨使用""引號建立的字串都是常量,編譯期就已經確定儲存到String Pool中;
(2)使用new String("")建立的物件會儲存到heap中,是執行期新建立的;
new建立字串時首先檢視池中是否有相同值的字串,如果有,則拷貝一份到堆中,然後返回堆中的地址;如果池中沒有,則在堆中建立一份,然後返回堆中的地址(注意,此時不需要從堆中複製到池中,否則,將使得堆中的字串永遠是池中的子集,導致浪費池的空間)!
(3)使用只包含常量的字串連線符如"aa" + "aa"建立的也是常量,編譯期就能確定,已經確定儲存到String Pool中;
(4)使用包含變數的字串連線符如"aa" + s1建立的物件是執行期才建立的,儲存在heap中;
4.使用String不一定建立物件
在執行到雙引號包含字串的語句時,如String a = "123",JVM會先到常量池裡查詢,如果有的話返回常量池裡的這個例項的引用,否則的話建立一個新例項並置入常量池裡。所以,當我們在使用諸如String str = "abc";的格式定義物件時,總是想當然地認為,建立了String類的物件str。擔心陷阱!物件可能並沒有被建立!而可能只是指向一個先前已經建立的物件。只有通過new()方法才能保證每次都建立一個新的物件。
5.使用new String,一定建立物件
在執行String a = new String("123")的時候,首先走常量池的路線取到一個例項的引用,然後在堆上建立一個新的String例項,走以下建構函式給value屬性賦值,然後把例項引用賦值給a:
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
從中我們可以看到,雖然是新建立了一個String的例項,但是value是等於常量池中的例項的value,即是說沒有new一個新的字元陣列來存放"123"。
6.關於String.intern()
intern方法使用:一個初始為空的字串池,它由類String獨自維護。當呼叫 intern方法時,如果池已經包含一個等於此String物件的字串(用equals(oject)方法確定),則返回池中的字串。否則,將此String物件新增到池中,並返回此String物件的引用。
它遵循以下規則:對於任意兩個字串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。
String.intern();
再補充介紹一點:存在於.class檔案中的常量池,在執行期間被jvm裝載,並且可以擴充。String的intern()方法就是擴充常量池的一個方法;當一個String例項str呼叫intern()方法時,java查詢常量池中是否有相同unicode的字串常量,如果有,則返回其引用,如果沒有,則在常量池中增加一個unicode等於str的字串並返回它的引用。
/**
* 關於String.intern()
*/
public void test11(){
String s0 = "kvill";
String s1 = new String("kvill");
String s2 = new String("kvill");
System.out.println("===========test11============");
System.out.println( s0 == s1 ); //false
System.out.println( "**********" );
s1.intern(); //雖然執行了s1.intern(),但它的返回值沒有賦給s1
s2 = s2.intern(); //把常量池中"kvill"的引用賦給s2
System.out.println( s0 == s1); //flase
System.out.println( s0 == s1.intern() ); //true//說明s1.intern()返回的是常量池中"kvill"的引用
System.out.println( s0 == s2 ); //true
}
執行結果:false、false、true、true。
7.關於equals和==
(1)對於==,如果作用於基本資料型別的變數(byte,short,char,int,long,float,double,boolean ),則直接比較其儲存的"值"是否相等;如果作用於引用型別的變數(String),則比較的是所指向的物件的地址(即是否指向同一個物件)。
(2)equals方法是基類Object中的方法,因此對於所有的繼承於Object的類都會有該方法。在Object類中,equals方法是用來比較兩個物件的引用是否相等,即是否指向同一個物件。
(3)對於equals方法,注意:equals方法不能作用於基本資料型別的變數。如果沒有對equals方法進行重寫,則比較的是引用型別的變數所指向的物件的地址;而String類對equals方法進行了重寫,用來比較指向的字串物件所儲存的字串是否相等。其他的一些類諸如Double,Date,Integer等,都對equals方法進行了重寫用來比較指向的物件所儲存的內容是否相等。
8 .String、StringBuffer、StringBuilder的區別
(1)可變與不可變:String是不可變字串物件,StringBuilder和StringBuffer是可變字串物件(其內部的字元陣列長度可變)。
(2)是否多執行緒安全:String中的物件是不可變的,也就可以理解為常量,顯然執行緒安全。StringBuffer 與 StringBuilder 中的方法和功能完全是等價的,只是StringBuffer 中的方法大都採用了synchronized 關鍵字進行修飾,因此是執行緒安全的,而 StringBuilder 沒有這個修飾,可以被認為是非執行緒安全的。
(3)String、StringBuilder、StringBuffer三者的執行效率:
StringBuilder > StringBuffer > String 當然這個是相對的,不一定在所有情況下都是這樣。比如String str = "hello"+ "world"的效率就比 StringBuilder st = new StringBuilder().append("hello").append("world")要高。因此,這三個類是各有利弊,應當根據不同的情況來進行選擇使用:
當字串相加操作或者改動較少的情況下,建議使用 String str="hello"這種形式;
當字串相加操作較多的情況下,建議使用StringBuilder,如果採用了多執行緒,則使用StringBuffer。
9 String中的final用法
final StringBuffer a = new StringBuffer("111"); final StringBuffer b = new StringBuffer("222"); a=b;//此句編譯不通過 final StringBuffer a = new StringBuffer("111"); a.append("222");//編譯通過
10.字串池的優缺點:
字串池的優點就是避免了相同內容的字串的建立,節省了記憶體,省去了建立相同字串的時間,同時提升了效能;另一方面,字串池的缺點就是犧牲了JVM在常量池中遍歷物件所需要的時間,不過其時間成本相比而言比較低。
public class StringTest {
public static void main(String[] args) {
/**
* 情景一:字串池
* JAVA虛擬機器(JVM)中存在著一個字串池,其中儲存著很多String物件;
* 並且可以被共享使用,因此它提高了效率。
* 由於String類是final的,它的值一經建立就不可改變。
* 字串池由String類維護,我們可以呼叫intern()方法來訪問字串池。
*/
String s1 = "abc";
//↑ 在字串池建立了一個物件
String s2 = "abc";
//↑ 字串pool已經存在物件“abc”(共享),所以建立0個物件,累計建立一個物件
System.out.println("s1 == s2 : "+(s1==s2));
//↑ true 指向同一個物件,
System.out.println("s1.equals(s2) : " + (s1.equals(s2)));
//↑ true 值相等
//↑------------------------------------------------------over
/**
* 情景二:關於new String("")
*
*/
String s3 = new String("abc");
//↑ 建立了兩個物件,一個存放在字串池中,一個存在與堆區中;
//↑ 還有一個物件引用s3存放在棧中
String s4 = new String("abc");
//↑ 字串池中已經存在“abc”物件,所以只在堆中建立了一個物件
System.out.println("s3 == s4 : "+(s3==s4));
//↑false s3和s4棧區的地址不同,指向堆區的不同地址;
System.out.println("s3.equals(s4) : "+(s3.equals(s4)));
//↑true s3和s4的值相同
System.out.println("s1 == s3 : "+(s1==s3));
//↑false 存放的地區多不同,一個棧區,一個堆區
System.out.println("s1.equals(s3) : "+(s1.equals(s3)));
//↑true 值相同
//↑------------------------------------------------------over
/**
* 情景三:
* 由於常量的值在編譯的時候就被確定(優化)了。
* 在這裡,"ab"和"cd"都是常量,因此變數str3的值在編譯時就可以確定。
* 這行程式碼編譯後的效果等同於: String str3 = "abcd";
*/
String str1 = "ab" + "cd"; //1個物件
String str11 = "abcd";
System.out.println("str1 = str11 : "+ (str1 == str11));
//↑------------------------------------------------------over
/**
* 情景四:
* 區域性變數str2,str3儲存的是儲存兩個拘留字串物件(intern字串物件)的地址。
*
* 第三行程式碼原理(str2+str3):
* 執行期JVM首先會在堆中建立一個StringBuilder類,
* 同時用str2指向的拘留字串物件完成初始化,
* 然後呼叫append方法完成對str3所指向的拘留字串的合併,
* 接著呼叫StringBuilder的toString()方法在堆中建立一個String物件,
* 最後將剛生成的String物件的堆地址存放在區域性變數str3中。
*
* 而str5儲存的是字串池中"abcd"所對應的拘留字串物件的地址。
* str4與str5地址當然不一樣了。
*
* 記憶體中實際上有五個字串物件:
* 三個拘留字串物件、一個String物件和一個StringBuilder物件。
*/
String str2 = "ab"; //1個物件
String str3 = "cd"; //1個物件
String str4 = str2+str3;
String str5 = "abcd";
System.out.println("str4 = str5 : " + (str4==str5)); // false
//↑------------------------------------------------------over
/**
* 情景五:
* JAVA編譯器對string + 基本型別/常量 是當成常量表達式直接求值來優化的。
* 執行期的兩個string相加,會產生新的物件的,儲存在堆(heap)中
*/
String str6 = "b";
String str7 = "a" + str6;
String str67 = "ab";
System.out.println("str7 = str67 : "+ (str7 == str67));
//↑str6為變數,在執行期才會被解析。
final String str8 = "b";
String str9 = "a" + str8;
String str89 = "ab";
System.out.println("str9 = str89 : "+ (str9 == str89));
//↑str8為常量變數,編譯期會被優化
//↑------------------------------------------------------over
}
}
執行結果:
s1 == s2 : true
s1.equals(s2) : true
s3 == s4 : false
s3.equals(s4) : true
s1 == s3 : false
s1.equals(s3) : true
str1 = str11 : true
str4 = str5 : false
str7 = str67 : false
str9 = str89 : true