|
Java中有一個(gè)String類,特別讓人傷腦筋。因?yàn)樗梢灾苯淤x值,也可以new一下用構(gòu)造器生成對(duì)象,還可以用加號(hào)拼接……這些不同的方式到底有什么區(qū)別?本文是個(gè)人學(xué)習(xí)的一些總結(jié),也希望能用最通俗的語(yǔ)言讓大家明白這個(gè)類。
一、字符串的創(chuàng)建:字符串創(chuàng)建有兩種方式,分別來(lái)看看這兩種方式有何區(qū)別: 1. 字面量賦值創(chuàng)建: String str1 = "hello";
String str2 = "hello";
String str3 = "world";
這樣創(chuàng)建字符串,首先會(huì)去常量池里找有沒(méi)有這個(gè)字符串,有就直接指向常量池的該字符串,沒(méi)有就先往常量池中添加一個(gè),再指向它。圖解: 2. 用new創(chuàng)建: String str1 = new String("hello");
String str2 = new String("hello");
String str3 = new String("world");
new一個(gè)字符串時(shí),做了兩件事。首先在堆中生成了該字符串對(duì)象,然后去看常量池中有沒(méi)有該字符串,如果有就不管了,沒(méi)有就往常量池中添加一個(gè)。圖解: 所以當(dāng)問(wèn)到“執(zhí)行上面那三行代碼創(chuàng)建了幾個(gè)對(duì)象”這樣的問(wèn)題就很簡(jiǎn)單了,堆中三個(gè)常量池中兩個(gè),總共是5個(gè)。 小結(jié):這兩種方式創(chuàng)建出來(lái)的,一個(gè)在堆中,一個(gè)在常量池中,所以它們之間用 == 比較肯定是false。 二、字符串的拼接:字符串可以直接用加號(hào)進(jìn)行拼接,但是也有幾種不同的情況。 1. 常量拼接 String str = "hello" + "world";
對(duì)于這種加號(hào)兩邊都是常量的,在編譯階段就會(huì)自動(dòng)拼接,變成 String str = "helloworld";
所以就會(huì)去常量池找"helloworld",有就直接指向它,沒(méi)有就在常量池創(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ì)象。所以這兩條語(yǔ)句總共生成了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"。所以這兩條語(yǔ)句總共創(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ì)象拷貝到常量池??聪旅胬樱?/p> String str1 = new String("str")+new String("01");
str1.intern();
String str2 = "str01";
System.out.println(str2==str1);
圖解上述代碼: 首先 newString("str")會(huì)在堆中創(chuàng)建str,同時(shí)添加到常量池;newString("01")也是一樣的,在堆中創(chuàng)建01,同時(shí)添加到常量池;然后兩者拼接,底層用的append方法,在堆中生成一個(gè)str01;然后 str1.intern(),就把str01拷貝到常量池了;此時(shí)運(yùn)行到 Stringstr2="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í)候,就沒(méi)有再進(jìn)行拷貝了。最終還是str1指向堆中的str01,str2指向常量池的str01,所以結(jié)果還是false。 2、JDK1.7以后(包括1.7):從JDK 1.7開(kāi)始,intern方法做了些改變,進(jìn)行拷貝的時(shí)候不是拷貝對(duì)象,而是拷貝地址值??聪旅娴睦樱?/p> 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í)行 Stringstr2=str01的時(shí)候,去常量池找str01,發(fā)現(xiàn)常量池中有 x001地址值,剛好該地址值對(duì)應(yīng)的就是要找的str01,就直接拿過(guò)來(lái)用。最終就是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);
同樣將二三行代碼換一下位置,看看是什么情況: 第一步就不多說(shuō)了,執(zhí)行第二步時(shí),往常量池中找str01,發(fā)現(xiàn)沒(méi)有,那就添加一個(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ì)有什么變化: 本來(lái)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方法??聪旅娲a: 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,也就是說(shuō)要new十次,會(huì)創(chuàng)建十個(gè)對(duì)象,這樣就會(huì)占用大量的資源。所以要讓其編譯后創(chuàng)建StringBuilder對(duì)象的過(guò)程在循環(huán)外面,代碼就該這樣寫(xiě): 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());
這樣就沒(méi)有在循環(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)指正!
|