前言:
Java中有一個(gè)String類,特別讓人傷腦筋鲁冯。因?yàn)樗梢灾苯淤x值拷沸,也可以new一下用構(gòu)造器生成對(duì)象,還可以用加號(hào)拼接……這些不同的方式到底有什么區(qū)別薯演?本文是個(gè)人學(xué)習(xí)的一些總結(jié)撞芍,也希望能用最通俗的語言讓大家明白這個(gè)類。
歡迎大家關(guān)注我的公眾號(hào) javawebkf跨扮,目前正在慢慢地將簡(jiǎn)書文章搬到公眾號(hào)序无,以后簡(jiǎn)書和公眾號(hào)文章將同步更新,且簡(jiǎn)書上的付費(fèi)文章在公眾號(hào)上將免費(fèi)衡创。
一帝嗡、字符串的創(chuàng)建:
字符串創(chuàng)建有兩種方式,分別來看看這兩種方式有何區(qū)別:
1. 字面量賦值創(chuàng)建:
String str1 = "hello";
String str2 = "hello";
String str3 = "world";
這樣創(chuàng)建字符串璃氢,首先會(huì)去常量池里找有沒有這個(gè)字符串哟玷,有就直接指向常量池的該字符串,沒有就先往常量池中添加一個(gè)一也,再指向它碗降。圖解:
2. 用new創(chuàng)建:
String str1 = new String("hello");
String str2 = new String("hello");
String str3 = new String("world");
new一個(gè)字符串時(shí),做了兩件事塘秦。首先在堆中生成了該字符串對(duì)象,然后去看常量池中有沒有該字符串动看,如果有就不管了尊剔,沒有就往常量池中添加一個(gè)。圖解:
所以當(dāng)問到“執(zhí)行上面那三行代碼創(chuàng)建了幾個(gè)對(duì)象”這樣的問題就很簡(jiǎn)單了菱皆,堆中三個(gè)常量池中兩個(gè)须误,總共是5個(gè)。
小結(jié):這兩種方式創(chuàng)建出來的仇轻,一個(gè)在堆中京痢,一個(gè)在常量池中,所以它們之間用 == 比較肯定是false篷店。
二祭椰、字符串的拼接:
字符串可以直接用加號(hào)進(jìn)行拼接臭家,但是也有幾種不同的情況。
1. 常量拼接
String str = "hello" + "world";
對(duì)于這種加號(hào)兩邊都是常量的方淤,在編譯階段就會(huì)自動(dòng)拼接钉赁,變成
String str = "helloworld";
所以就會(huì)去常量池找"helloworld",有就直接指向它携茂,沒有就在常量池創(chuàng)建再指向你踩。
2. 有final的拼接:
final String str1 = "hello";
final String str2 = "world";
String str3 = str1 + str2;
因?yàn)閒inal修飾的變量就是常量,所以在編譯期直接會(huì)變成
String str3 = "hello" + "world";
再根據(jù)常量拼接規(guī)則可知最終就變成
String str3 = "helloworld";
3. 變量和常量拼接:
變量和常量拼接的時(shí)候讳苦,底層會(huì)調(diào)用StringBuilder的append方法生成新對(duì)象带膜。
- 情況一:
String str1 = "hello";
String str2 = str1 + "world";
str1顯然是在常量池中的,world也是在常量池中的鸳谜,然后調(diào)用append方法在堆中生成新對(duì)象"helloworld"膝藕,str2就指向堆中的"helloworld"對(duì)象。所以這兩條語句總共生成了3個(gè)對(duì)象卿堂,常量池中有"hello"和"world"束莫,堆中有"helloword"。
- 情況二:
String str1 = new String("hello");
String str2 = str1 + "world";
首先會(huì)在堆中創(chuàng)建一個(gè)"hello"草描,再把"hello"添加到常量池览绿;然后會(huì)把"world"添加到常量池,拼接的時(shí)候穗慕,會(huì)在堆中創(chuàng)建一個(gè)"helloworld"饿敲。所以這兩條語句總共創(chuàng)建了4個(gè)對(duì)象,堆中的"hello"逛绵、"helloworld"和常量池中的"hello"怀各、"world"。
4. 變量和變量拼接:
變量和變量拼接术浪,底層也會(huì)調(diào)用StringBuilder的append方法生成新對(duì)象瓢对。
- 情況一:
String str1 = "hello";
String str2 = "world";
String str3 = str1 + str2;
這段代碼,首先會(huì)有一個(gè)"hello"在常量池中胰苏,然后有個(gè)"world"在常量池硕蛹,第三行代碼會(huì)調(diào)用append方法,在堆中生成一個(gè)"helloworld"硕并。所以總共有3個(gè)對(duì)象法焰。
- 情況二:
String str1 = "hello";
String str2 = new String("world");
String str3 = str1 + str2;
這段代碼,首先在常量池中搞一個(gè)"hello"倔毙,然后在堆中new一個(gè)"world"埃仪,同時(shí)把"world"也搞到常量池中去,第三步拼接就會(huì)在堆中生成一個(gè)"helloworld"陕赃。所以總共有4個(gè)對(duì)象卵蛉。
- 情況三:
String str1 = new String("hello");
String str2 = new String("world");
String str3 = str1 + str2;
第一行代碼創(chuàng)建了兩個(gè)對(duì)象颁股,堆中一個(gè)常量池一個(gè),第二行代碼也是一樣毙玻,第三行代碼就在堆中創(chuàng)建了一個(gè)"helloworld"豌蟋。所以總共創(chuàng)建了5個(gè)對(duì)象。
三桑滩、intern方法:
1梧疲、Java 1.7以前:
JDK 1.7以前,intern方法會(huì)把對(duì)象拷貝到常量池运准』系看下面例子:
- 例一:
String str1 = new String("str")+new String("01");
str1.intern();
String str2 = "str01";
System.out.println(str2==str1);
圖解上述代碼:
首先new String("str")
會(huì)在堆中創(chuàng)建str,同時(shí)添加到常量池胁澳;new String("01")
也是一樣的该互,在堆中創(chuàng)建01,同時(shí)添加到常量池韭畸;然后兩者拼接宇智,底層用的append方法,在堆中生成一個(gè)str01胰丁;然后str1.intern()
随橘,就把str01拷貝到常量池了;此時(shí)運(yùn)行到String str2 = "str01"
锦庸,發(fā)現(xiàn)常量池中有了机蔗,所以直接指向常量池中的str01。最終str1指向堆中的str01對(duì)象甘萧,str2指向常量池的str01對(duì)象萝嘁,所以結(jié)果是false。
- 例二:
String str1 = new String("str")+new String("01");
String str2 = "str01";
str1.intern();
System.out.println(str2==str1);
我們將第二三行代碼調(diào)換順序扬卷,看看情況有什么不同:
換一下順序牙言,區(qū)別就在于執(zhí)行到第二行代碼的時(shí)候,常量池中就已經(jīng)有str01了怪得,所以再執(zhí)行str1.intern()
的時(shí)候嬉挡,就沒有再進(jìn)行拷貝了。最終還是str1指向堆中的str01汇恤,str2指向常量池的str01,所以結(jié)果還是false拔恰。
2因谎、JDK1.7以后(包括1.7):
從JDK 1.7開始,intern方法做了些改變颜懊,進(jìn)行拷貝的時(shí)候不是拷貝對(duì)象财岔,而是拷貝地址值风皿。看下面的例子:
- 例一:
String str1 = new String("str")+new String("01");
str1.intern();
String str2 = "str01";
System.out.println(str2==str1);
圖解上述代碼:
第一步和JDK 1.7之前是一樣的匠璧,現(xiàn)在堆中創(chuàng)建一個(gè)str桐款,同時(shí)搞到常量池,再創(chuàng)建一個(gè)01夷恍,同時(shí)搞到常量池魔眨,然后拼接,在堆中生成對(duì)象str01酿雪;不同的就是
str1.intern()
遏暴,這次拷貝的不是str01這個(gè)對(duì)象,而是把它的地址值搞到常量池中去了指黎;然后執(zhí)行String str2 = str01
的時(shí)候朋凉,去常量池找str01,發(fā)現(xiàn)常量池中有x001
地址值醋安,剛好該地址值對(duì)應(yīng)的就是要找的str01杂彭,就直接拿過來用。最終就是str1指向地址值為x001
的對(duì)象吓揪,str2也是指向地址值為x001
的對(duì)象亲怠,所以結(jié)果是true。
- 例二:
String str1 = new String("str")+new String("01");
String str2 = "str01";
str1.intern();
System.out.println(str2==str1);
同樣將二三行代碼換一下位置磺芭,看看是什么情況:
第一步就不多說了赁炎,執(zhí)行第二步時(shí),往常量池中找str01钾腺,發(fā)現(xiàn)沒有徙垫,那就添加一個(gè);再執(zhí)行
str1.intern()
時(shí)放棒,發(fā)現(xiàn)常量池中有str01了姻报,就不進(jìn)行地址值的拷貝了。最終str1指向堆中的str01间螟,str2指向常量池的str01吴旋,所以結(jié)果是false。
- 例三:
String str1 = new String("str")+new String("01");
String str2 = "str01";
str1 = str1.intern();
System.out.println(str2==str1);
就是把例二的str1.intern()
改成str1 = str1.intern()
厢破,看看會(huì)有什么變化:
本來str1是指向堆中的str01的荣瑟,然后重新將
str1.intern()
賦給str1,str1.intern()
是指向常量池的摩泪,賦給str1后笆焰,所以此時(shí)str1也是指向常量池。所以結(jié)果就是true见坑。
四嚷掠、String捏检、StringBuilder和StringBuffer:
String和后兩者的區(qū)別就是String是不可變的,后兩者可變不皆。StringBuilder是JDK 1.5以后提供的贯城,以前用StringBuffer。StringBuffer和StringBuilder的功能基本一樣霹娄,只是StringBuffer是線程安全的能犯,而StringBuilder不是線程安全的。因此项棠,StringBuilder的效率會(huì)更高悲雳。
上面字符串拼接部分的案例都是用加號(hào)拼接的,然后也提到了StringBuilder的append方法香追。其實(shí)就算是加號(hào)拼接合瓢,底層還是用的StringBuilder的append方法⊥傅洌看下面代碼:
String s = "abc";
String ss = "ok" + s + "xyz" + 5;
這就用加號(hào)拼接的例子晴楔,利用反編譯工具看看這段代碼到底編譯成了啥:
String s = "abc";
String ss = (new StringBuilder("ok")).append(s).append("xyz").append(5).toString();
可看到,編譯后是用StringBuilder的append方法進(jìn)行拼接的峭咒。那么使用加號(hào)和使用append方法到底有什么區(qū)別呢税弃?看一下以下代碼:
String s = "";
Random rand = new Random();
for (int i = 0; i < 10; i++){
s = s + rand.nextInt(1000) + " ";
}
System.out.println(s);
這個(gè)例子很簡(jiǎn)單,就是在循環(huán)里面用加號(hào)進(jìn)行字符串的拼接凑队,看一下反編譯后是什么樣子的:
String s = "";
Random rand = new Random();
for(int i = 0; i < 10; i++) {
s = (new StringBuilder(String.valueOf(s))).append(rand.nextInt(1000)).append(" ").toString();
}
System.out.println(s);
可以看到则果,它是在循環(huán)里面new了StringBuilder對(duì)象,然后用其append方法進(jìn)行拼接漩氨。這里是i從0到9西壮,也就是說要new十次,會(huì)創(chuàng)建十個(gè)對(duì)象叫惊,這樣就會(huì)占用大量的資源款青。所以要讓其編譯后創(chuàng)建StringBuilder對(duì)象的過程在循環(huán)外面,代碼就該這樣寫:
String s = "";
Random rand = new Random();
StringBuilder result = new StringBuilder();
for (int i = 0; i < 10; i++){
result.append(rand.nextInt(1000));
result.append(" ");
}
System.out.println(result.toString());
那么編譯后就是這樣的:
String s = "";
Random rand = new Random();
StringBuilder result = new StringBuilder();
for(int i = 0; i < 10; i++) {
result.append(rand.nextInt(1000));
result.append(" ");
}
System.out.println(result.toString());
這樣就沒有在循環(huán)里面new對(duì)象了霍狰。
小結(jié):當(dāng)要在循環(huán)里面進(jìn)行字符串拼接的時(shí)候抡草,就該先在循環(huán)外面new一個(gè)StringBuilder,然后在循環(huán)里面用append進(jìn)行拼接蔗坯;其他情況就可以使用加號(hào)進(jìn)行拼接更加簡(jiǎn)單。
總結(jié):
本文用圖文形式講了String的面試考點(diǎn)宾濒,特別要注意JDK版本不同intern方法的差異。還有就是常量池的位置到底在方法區(qū)還是在堆中還是在元空間,這個(gè)我也不是很清楚答姥,網(wǎng)上搜索的答案也比較雜谚咬。以上內(nèi)容如果有誤,歡迎批評(píng)指正择卦!