一、字串相關的類
1.1 String 的特性
- String類:代表字串。Java 程式中的所有字串字面值(如 "abc" )都作為此類的例項實現。
- String是一個final類,代表不可變的字元序列。
- final修飾的類不能被繼承
- 字串是常量,用雙引號引起來表示。它們的值在建立之後不能更改。
- String物件的字元內容是儲存在一個字元陣列value[]中的。
- 資料儲存結構中,有鏈式儲存結構和順序儲存結構兩種。顯然String底層選擇了char型陣列型別的順序儲存結構。
1.2 String部分原始碼
Windows電腦,idea中。雙擊shift,輸入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
/** use serialVersionUID from JDK 1.0.2 for interoperability */
private static final long serialVersionUID = -6849794470754667710L;
....
}
從這部分原始碼中,我們就可以很好的理解“1.1 String 的特性”中的內容了!!!字串,字串,就是字元構建起來的串。
1.3 String再次理解
1.3.1 上程式碼
public class StringTest {
@Test
public void test1() {
// String name = "abc"; 字面量的定義方式
String name = "abc";
String des = "abc";
name = "hell world";
System.out.println("name:"+name);// hello world
System.out.println("des:"+des);// abc
}
@Test
public void test2() {
final int a = 2;
a = 3; // 編譯不通過
}
}
-
第一個問題:為什麼name = "hell world",編譯成功,並能執行,不是說“它們的值在建立之後不能更改”嗎?;而test2()中卻編譯失敗?
-
首先,String屬性引用型別,而int是基本型別。
-
引用型別變數儲存地址值,而a是一個指向int型別的引用,指向2這個字面值。因此final int 修飾的變數就變為常量了,常量是不能修改其值的,所以test2()編譯失敗。
-
那麼怎麼理解String 底層儲存使用final修飾的char型陣列,更改其值為什麼編譯成功,並能執行?簡單的JVM走一波。圖一:
從圖中我們可以看出:
- 常量池當中,是不會儲存兩個相同的字串的。
- name、des儲存的是地址值。name和des都指向0x8888這個地址
圖二:
從圖中我們可以看出:
- 這就很好解釋了為什麼輸出結果是name為hello world 而des依然為abc。
- 同時也解釋了“它們的值在建立之後不能更改”,原始的值“abc”的確沒有被修改,而是在字串常量池中重新建立了一份。
- 這也可以看出頻繁的對字串進行增刪改操作的話,很耗費記憶體資源。
-
1.3.2 初步總結
- String:字串,使用一對""引起來表示。
- String宣告為final的,不可被繼承。
- String實現了Serializable介面:表示字串是支援序列化的。
- 實現了Comparable介面:表示String可以比較大小
- String內部定義了final char[] value用於儲存字串資料
- String:代表不可變的字元序列。簡稱:不可變性。
- 當對字串重新賦值時,需要重寫指定記憶體區域賦值,不能使用原有的value進行賦值。
- 當對現有的字串進行連線操作時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
- 當呼叫String的replace()方法修改指定字元或字串時,也需要重新指定記憶體區域賦值,不能使用原有的value進行賦值。
- 通過字面量的方式(區別於new)給一個字串賦值,此時的字串值宣告在字串常量池中。
- 字串常量池中是不會儲存相同內容的字串的。
1.4 判斷對錯
@Test
public void test3() {
String s1="javaWeb";
String s2="javaWeb";
String s3 = new String("javaWeb");
String s4 = new String("javaWeb");
System.out.println(s1==s2);
System.out.println(s1==s3);
System.out.println(s1==s4);
System.out.println(s3==s4);
}
直觀判斷:
- s1==s2。在字串常量池中相同的字串只會儲存一份,s1和s2儲存相同的地址。所以為true
- s1==s3。他們分別指向不同地方,所以為false
- s1==s4。他們分別指向不同地方,所以為false
- s3==s4。只要是new便會在堆空間中開闢一份空間,JVM才不會管你new的內容是否和前面的相同。所以為false
集合JVM判斷:
結合JVM圖形就可以很清楚的明白到底為什麼錯,為什麼對了。
1.5 判斷物件的屬性
public class Person {
String name;
int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
@Test
public void test4() {
Person p1 = new Person("Tom", 12);
Person p2 = new Person("Tom", 12);
System.out.println(p1.name==p2.name);// true?false?
}
顯然為true,Tom儲存在字串常量池中,有且僅有一份。故p1.name==p2.name必然為true
再來:
@Test
public void test4() {
Person p1 = new Person("Tom", 12);
Person p2 = new Person("Tom", 12);
System.out.println(p1.name==p2.name);
p2.name="cxk";
System.out.println(p1.name==p2.name);// true?false
}
肯定是false, p2.name="cxk";會先在字串常量池中查詢,沒有就在常量池中建一個, p2.name地址指向它即可。【字串的不可變性】
1.6 擴充:
-
String s = new String("abc");方式建立物件,在記憶體中建立了幾個物件?
答:2個。一個是堆空間中new結構,另一個是char[ ]對應的常量池中的資料:”abc“;
-
來,搞一下這個。
public void test3(){ String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; String s4 = "javaEE" + "hadoop"; String s5 = s1 + "hadoop"; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4); System.out.println(s3 == s5); System.out.println(s3 == s6); System.out.println(s3 == s7); System.out.println(s5 == s6); System.out.println(s5 == s7); System.out.println(s6 == s7); String s8 = s6.intern(); System.out.println(s3 == s8); }
- s3 == s4為true。字面量或者字面量之間的連線,指向同一個物件,在常量池中宣告。
- s5、s6、s7都是字面量拼接變數或者變數拼接變數,他們不是在常量池中宣告而是在堆中宣告。可以把它想象為new,從而儘管內容相同,但是地址值卻是是不相同的。
結論:
- 常量與常量的拼接結果在常量池。且常量池中不會存在相同內容的常量。 只要其中有一個是變數,結果就在堆中
- 如果拼接的結果呼叫intern()方法,返回值就在常量池中
1.7 面試題
程式碼一:
public class StringTest2 {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public void change(String str, char ch[]) {
str = "test ok";
System.out.println("======"+str);
ch[0] = 'b';
}
public static void main(String[] args) {
StringTest2 ex = new StringTest2();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.println(ex.ch);
}
}
輸出結果是多少?
解析:做這道題必須明白幾個知識點
- java中方法引數的傳遞機制——值傳遞。
- 值傳遞。即將實際引數的副本(複製品)傳入到方法內,而引數本身不受影響。
- 形參是基本資料型別:將實參基本資料型別變數的“資料值”傳遞給形參【原始值不受影響】
- 形參是引用資料型別:將實參引用資料型別變數的“地址值”傳遞給形參【因為傳遞副本為地址值,所以某些原始值會受影響,某些不會,如String型別具有不可變性】
- 值傳遞。即將實際引數的副本(複製品)傳入到方法內,而引數本身不受影響。
- String型別具有不可變性。
分析:
程式碼二:
public class StringTest2 {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public void change(String str, char ch[]) {
// 第二步:
//對於方法中的str,由於第一步的賦值操作,它的地址值和“String str = new String("good");”中的str一樣,內容都為good。
//接下來方法中對它進行賦值操作“test ok”。
//由於String型別的不可變性(看原始碼類為final修飾,資料儲存結構為也為final修飾的char型陣列),不可修改。
//因此JVM執行'str ="test ok";'時會在字串常量池中新建一個“test ok”並且更新方法中str的地址值。
//至此兩個str的地址值不同了,指向的內容也不同了。 可以看“程式碼三”中的對比結果。
str = "test ok";
// 第二步;
//對於方法中的ch,由於第一步的賦值操作,它的地址值和“char[] ch = {'t', 'e', 's', 't'};”中的ch一樣,內容都為test。
//接下來,在方法中執行"ch[0] = 'b';" 陣列中的第一個元素被賦值為b。由於是引用型別,此時類中成員變數char[] ch = {'t', 'e', 's', 't'};值也變為best。
ch[0] = 'b';
}
public static void main(String[] args) {
StringTest2 ex = new StringTest2();
// 第一步
// 傳遞的兩個實參,引數一,實際上是把StringTest2類中成員變數str的地址值複製一份給StringTest2類中change方法中的形參變數str
// 同理,StringTest2類中成員變數ch(陣列:也是引用型別)的地址值複製一份給StringTest2類中change方法中的形參變數ch[]
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.println(ex.ch);
}
}
程式碼三:
public class StringTest2 {
String str = new String("good");
char[] ch = {'t', 'e', 's', 't'};
public void change(String str, char ch[]) {
System.out.print("賦值之前兩個str的地址值比較為:");
System.out.println(this.str==str);
str = "test ok";
System.out.print("賦值之後兩個str的地址值比較為:");
System.out.println(this.str==str);
System.out.println("====================分割線========================");
ch[0] = 'b';
}
public static void main(String[] args) {
StringTest2 ex = new StringTest2();
ex.change(ex.str, ex.ch);
System.out.print(ex.str + " and ");
System.out.println(ex.ch);
}
}
結果: