**一、字符串問題**
字符串在我們平時的編碼工作中其實用的非常多,并且用起來也比較簡單,所以很少有人對其做特別深入的研究。倒是面試或者筆試的時候,往往會涉及比較深入和難度大一點的問題。我在招聘的時候也偶爾會問應聘者相關的問題,倒不是說一定要回答的特別正確和深入,通常問這些問題的目的有兩個,第一是考察對 JAVA 基礎知識的了解程度,第二是考察應聘者對技術的態度。
我們看看以下程序會輸出什么結果?如果你能正確的回答每一道題,并且清楚其原因,那本文對你就沒什么太大的意義。如果回答不正確或者不是很清楚其原理,那就仔細看看以下的分析,本文應該能幫助你清楚的理解每段程序的結果及輸出該結果的深層次原因。
代碼段一:
~~~
`package`?`com.paddx.test.string;`
`public`?`class`?`StringTest {`
`public`?`static`?`void`?`main(String[] args) {`
`String str1 =?``"string"``;`
`String str2 =?``new`?`String(``"string"``);`
`String str3 = str2.intern();`
`System.out.println(str1==str2);``//#1`
`System.out.println(str1==str3);``//#2`
`}`
`}`
~~~
?代碼段二:
~~~
`package`?`com.paddx.test.string;`
`public`?`class`?`StringTest01 {`
`public`?`static`?`void`?`main(String[] args) {`
`String baseStr =?``"baseStr"``;`
`final`?`String baseFinalStr =?``"baseStr"``;`
`String str1 =?``"baseStr01"``;`
`String str2 =?``"baseStr"``+``"01"``;`
`String str3 = baseStr +?``"01"``;`
`String str4 = baseFinalStr+``"01"``;`
`String str5 =?``new`?`String(``"baseStr01"``).intern();`
`System.out.println(str1 == str2);``//#3`
`System.out.println(str1 == str3);``//#4`
`System.out.println(str1 == str4);``//#5`
`System.out.println(str1 == str5);``//#6`
`}`
`}`
~~~
代碼段三(1):
~~~
`package`?`com.paddx.test.string;<br> `
`public`?`class`?`InternTest {`
`public`?`static`?`void`?`main(String[] args) {`
`String str2 =?``new`?`String(``"str"``)+``new`?`String(``"01"``);`
`str2.intern();`
`String str1 =?``"str01"``;`
`System.out.println(str2==str1);``//#7`
`}`
`}`
~~~
?代碼段三(2):
~~~
`package`?`com.paddx.test.string;`
`public`?`class`?`InternTest01 {`
`public`?`static`?`void`?`main(String[] args) {`
`String str1 =?``"str01"``;`
`String str2 =?``new`?`String(``"str"``)+``new`?`String(``"01"``);`
`str2.intern();`
`System.out.println(str2 == str1);``//#8`
`}`
`}`
~~~
為了方便描述,我對上述代碼的輸出結果由#1~#8進行了編碼,下文中藍色字體部分即為結果。
**二、字符串深入分析**
** 1、代碼段一分析**
字符串不屬于基本類型,但是可以像基本類型一樣,直接通過字面量賦值,當然也可以通過new來生成一個字符串對象。不過通過字面量賦值的方式和new的方式生成字符串有本質的區別:

通過字面量賦值創建字符串時,會優先在常量池中查找是否已經存在相同的字符串,倘若已經存在,棧中的引用直接指向該字符串;倘若不存在,則在常量池中生成一個字符串,再將棧中的引用指向該字符串。而通過new的方式創建字符串時,就直接在堆中生成一個字符串的對象(備注,JDK 7 以后,HotSpot 已將常量池從永久代轉移到了堆中。詳細信息可參考《[JDK8內存模型-消失的PermGen](http://www.cnblogs.com/paddix/p/5309550.html)》一文),棧中的引用指向該對象。對于堆中的字符串對象,可以通過 intern() 方法來將字符串添加的常量池中,并返回指向該常量的引用。
現在我們應該能很清楚代碼段一的結果了:
結果 #1:因為str1指向的是字符串中的常量,str2是在堆中生成的對象,所以str1==str2返回false。
結果 #2:str2調用intern方法,會將str2中值(“string”)復制到常量池中,但是常量池中已經存在該字符串(即str1指向的字符串),所以直接返回該字符串的引用,因此str1==str2返回true。
以下運行代碼段一的代碼的結果:

**?2、代碼段二分析**
對于代碼段二的結果,還是通過反編譯StringTest01.class文件比較容易理解:
?常量池內容(部分):

執行指令(部分,第二列#+序數對應常量池中的項):

在解釋上述執行過程之前,先了解兩條指令:
ldc:Push item from run-time constant pool,從常量池中加載指定項的引用到棧。
astore_:Store reference into local variable,將引用賦值給第n個局部變量。
現在我們開始解釋代碼段二的執行過程:
0: ldc ? ? ? ? ? #2:加載常量池中的第二項("baseStr")到棧中。
2: astore_1 ? ? ?:將1中的引用賦值給第一個局部變量,即String baseStr = "baseStr";
3: ldc ? ? ? ? ? #2:加載常量池中的第二項("baseStr")到棧中。
5: astore_2 ? ? ?:將3中的引用賦值給第二個局部變量,即 final String baseFinalStr="baseStr";?
6: ldc ? ? ? ? ? #3:加載常量池中的第三項("baseStr01")到棧中。
8: astore_3 ? ? :將6中的引用賦值給第三個局部變量,即String str1="baseStr01";
9: ldc ? ? ? ? ? #3:加載常量池中的第三項("baseStr01")到棧中。
11: astore ? ? ? ?4:將9中的引用賦值給第四個局部變量:即String str2="baseStr01";
結果#3:str1==str2 肯定會返回true,因為str1和str2都指向常量池中的同一引用地址。所以其實在JAVA 1.6之后,常量字符串的“+”操作,編譯階段直接會合成為一個字符串。
13: new ? ? ? ? ? #4:生成StringBuilder的實例。
16: dup? ? :復制13生成對象的引用并壓入棧中。
17: invokespecial #5:調用常量池中的第五項,即StringBuilder.方法。
以上三條指令的作用是生成一個StringBuilder的對象。
20: aload_1 :加載第一個參數的值,即"baseStr"
21: invokevirtual #6 :調用StringBuilder對象的append方法。
24: ldc ? ? ? ? ? #7:加載常量池中的第七項("01")到棧中。
26: invokevirtual #6:調用StringBuilder.append方法。
29: invokevirtual #8:調用StringBuilder.toString方法。
32: astore ? ? ? ?5:將29中的結果引用賦值改第五個局部變量,即對變量str3的賦值。
結果 #4:因為str3實際上是stringBuilder.append()生成的結果,所以與str1不相等,結果返回false。
34: ldc ? ? ? ? ? #3:加載常量池中的第三項("baseStr01")到棧中。
36: astore ? ? ? ?6:將34中的引用賦值給第六個局部變量,即str4="baseStr01";
結果 #5 :因為str1和str4指向的都是常量池中的第三項,所以str1==str4返回true。這里我們還能發現一個現象,對于final字段,編譯期直接進行了常量替換,而對于非final字段則是在運行期進行賦值處理的。
38: new ? ? ? ? ? #9:創建String對象
41: dup ? ? ? ? ? ? ? :復制引用并壓如棧中。
42: ldc ? ? ? ? ? #3:加載常量池中的第三項("baseStr01")到棧中。
44: invokespecial #10:調用String.""方法,并傳42步驟中的引用作為參數傳入該方法。
47: invokevirtual #11:調用String.intern方法。
從38到41的對應的源碼就是new String("baseStr01").intern()。
50: astore ? ? ? ?7:將47步返回的結果賦值給變量7,即str5指向baseStr01在常量池中的位置。
結果 #6 :因為str5和str1都指向的都是常量池中的同一個字符串,所以str1==str5返回true。
運行代碼段二,輸出結果如下:?

**?3、代碼段三解析:**
?對于代碼段三,在 JDK 1.6 和 JDK 1.7中的運行結果不同。我們先看一下運行結果,然后再來解釋其原因:
?JDK 1.6 下的運行結果:

JDK 1.7 下的運行結果:
?
根據對代碼段一的分析,應該可以很簡單得出 JDK 1.6 的結果,因為 str2 和 str1本來就是指向不同的位置,理應返回false。
比較奇怪的問題在于JDK 1.7后,對于第一種情況返回true,但是調換了一下位置返回的結果就變成了false。這個原因主要是從JDK 1.7后,HotSpot 將常量池從永久代移到了元空間,正因為如此,JDK 1.7 后的intern方法在實現上發生了比較大的改變,JDK 1.7后,intern方法還是會先去查詢常量池中是否有已經存在,如果存在,則返回常量池中的引用,這一點與之前沒有區別,區別在于,如果在常量池找不到對應的字符串,則不會再將字符串拷貝到常量池,而只是在常量池中生成一個對原字符串的引用。所以:
結果 #7:在第一種情況下,因為常量池中沒有“str01”這個字符串,所以會在常量池中生成一個對堆中的“str01”的引用,而在進行字面量賦值的時候,常量池中已經存在,所以直接返回該引用即可,因此str1和str2都指向堆中的字符串,返回true。
結果 #8:調換位置以后,因為在進行字面量賦值(String str1 = "str01")的時候,常量池中不存在,所以str1指向的常量池中的位置,而str2指向的是堆中的對象,再進行intern方法時,對str1和str2已經沒有影響了,所以返回false。
**三、常見面試題解答**
有了對以上的知識的了解,我們現在再來看常見的面試或筆試題就很簡單了:
Q:String s = new String("xyz"),創建了幾個String Object??
A:兩個,常量池中的"xyz"和堆中對象。
Q:下列程序的輸出結果:
String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);
A:true,均指向常量池中對象。
Q:下列程序的輸出結果:
String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);
A:false,兩個引用指向堆中的不同對象。
Q:下列程序的輸出結果:
String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:false,因為s2+s3實際上是使用StringBuilder.append來完成,會生成不同的對象。
Q:下列程序的輸出結果:
String s1 = “abc”;
final String s2 = “a”;
final String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);
A:true,因為final變量在編譯后會直接替換成對應的值,所以實際上等于s4="a"+"bc",而這種情況下,編譯器會直接合并為s4="abc",所以最終s1==s4。
Q:下列程序的輸出結果:
String s = new String("abc");
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s == s1.intern());
System.out.println(s == s2.intern());
System.out.println(s1 == s2.intern());
A:false,false,true,具體原因參考第二部分內容。
?作者:[liuxiaopeng](http://www.cnblogs.com/paddix)
?博客地址:[http://www.cnblogs.com/paddix/](http://www.cnblogs.com/paddix)
?聲明:轉載請在文章頁面明顯位置給出原文連接。?
- JVM
- 深入理解Java內存模型
- 深入理解Java內存模型(一)——基礎
- 深入理解Java內存模型(二)——重排序
- 深入理解Java內存模型(三)——順序一致性
- 深入理解Java內存模型(四)——volatile
- 深入理解Java內存模型(五)——鎖
- 深入理解Java內存模型(六)——final
- 深入理解Java內存模型(七)——總結
- Java內存模型
- Java內存模型2
- 堆內內存還是堆外內存?
- JVM內存配置詳解
- Java內存分配全面淺析
- 深入Java核心 Java內存分配原理精講
- jvm常量池
- JVM調優總結
- JVM調優總結(一)-- 一些概念
- JVM調優總結(二)-一些概念
- VM調優總結(三)-基本垃圾回收算法
- JVM調優總結(四)-垃圾回收面臨的問題
- JVM調優總結(五)-分代垃圾回收詳述1
- JVM調優總結(六)-分代垃圾回收詳述2
- JVM調優總結(七)-典型配置舉例1
- JVM調優總結(八)-典型配置舉例2
- JVM調優總結(九)-新一代的垃圾回收算法
- JVM調優總結(十)-調優方法
- 基礎
- Java 征途:行者的地圖
- Java程序員應該知道的10個面向對象理論
- Java泛型總結
- 序列化與反序列化
- 通過反編譯深入理解Java String及intern
- android 加固防止反編譯-重新打包
- volatile
- 正確使用 Volatile 變量
- 異常
- 深入理解java異常處理機制
- Java異常處理的10個最佳實踐
- Java異常處理手冊和最佳實踐
- Java提高篇——對象克隆(復制)
- Java中如何克隆集合——ArrayList和HashSet深拷貝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常見正則表達式
- 類
- 理解java類加載器以及ClassLoader類
- 深入探討 Java 類加載器
- 類加載器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之實現細節
- java.util.concurrent 之ConcurrentHashMap 源碼分析
- HashMap的實現原理和底層數據結構
- 線程
- 關于Java并發編程的總結和思考
- 40個Java多線程問題總結
- Java中的多線程你只要看這一篇就夠了
- Java多線程干貨系列(1):Java多線程基礎
- Java非阻塞算法簡介
- Java并發的四種風味:Thread、Executor、ForkJoin和Actor
- Java中不同的并發實現的性能比較
- JAVA CAS原理深度分析
- 多個線程之間共享數據的方式
- Java并發編程
- Java并發編程(1):可重入內置鎖
- Java并發編程(2):線程中斷(含代碼)
- Java并發編程(3):線程掛起、恢復與終止的正確方法(含代碼)
- Java并發編程(4):守護線程與線程阻塞的四種情況
- Java并發編程(5):volatile變量修飾符—意料之外的問題(含代碼)
- Java并發編程(6):Runnable和Thread實現多線程的區別(含代碼)
- Java并發編程(7):使用synchronized獲取互斥鎖的幾點說明
- Java并發編程(8):多線程環境中安全使用集合API(含代碼)
- Java并發編程(9):死鎖(含代碼)
- Java并發編程(10):使用wait/notify/notifyAll實現線程間通信的幾點重要說明
- java并發編程-II
- Java多線程基礎:進程和線程之由來
- Java并發編程:如何創建線程?
- Java并發編程:Thread類的使用
- Java并發編程:synchronized
- Java并發編程:Lock
- Java并發編程:volatile關鍵字解析
- Java并發編程:深入剖析ThreadLocal
- Java并發編程:CountDownLatch、CyclicBarrier和Semaphore
- Java并發編程:線程間協作的兩種方式:wait、notify、notifyAll和Condition
- Synchronized與Lock
- JVM底層又是如何實現synchronized的
- Java synchronized詳解
- synchronized 與 Lock 的那點事
- 深入研究 Java Synchronize 和 Lock 的區別與用法
- JAVA編程中的鎖機制詳解
- Java中的鎖
- TreadLocal
- 深入JDK源碼之ThreadLocal類
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的內存泄露
- 多線程設計模式
- Java多線程編程中Future模式的詳解
- 原子操作(CAS)
- [譯]Java中Wait、Sleep和Yield方法的區別
- 線程池
- 如何合理地估算線程池大小?
- JAVA線程池中隊列與池大小的關系
- Java四種線程池的使用
- 深入理解Java之線程池
- java并發編程III
- Java 8并發工具包漫游指南
- 聊聊并發
- 聊聊并發(一)——深入分析Volatile的實現原理
- 聊聊并發(二)——Java SE1.6中的Synchronized
- 文件
- 網絡
- index
- 內存文章索引
- 基礎文章索引
- 線程文章索引
- 網絡文章索引
- IOC
- 設計模式文章索引
- 面試
- Java常量池詳解之一道比較蛋疼的面試題
- 近5年133個Java面試問題列表
- Java工程師成神之路
- Java字符串問題Top10
- 設計模式
- Java:單例模式的七種寫法
- Java 利用枚舉實現單例模式
- 常用jar
- HttpClient和HtmlUnit的比較總結
- IO
- NIO
- NIO入門
- 注解
- Java Annotation認知(包括框架圖、詳細介紹、示例說明)