在寫之前我們先來看幾個問題,假如你對這些問題已經很懂了的話,那大可不用看這篇文章,如果不大懂的話,那麼可以看看我的想法。
問題1:
public static void main(String[] args){
String t1 = new String("2");
t1.intern();
String t2 = "2";
System.out.println(t1 == t2);
String t3 = new String("2") + new String("2");
t3.intern();
String t4 = "22";
System.out.println(t3 == t4);
}
答案輸出:
JDK1.6是 false false
JDK1.7是 false true;
問題2(把問題1的語句調換一下位置)
public static void main(String[] args){
String t1 = new String("2");
String t2 = "2";
t1.intern();
System.out.println(t1 == t2);
String t3 = new String("2") + new String("2");
String t4 = "22";
t3.intern();
System.out.println(t3 == t4);
}
答案輸出:
false false
對於這兩個問題,看了幾個人的部落格,可謂百花齊放,越看越懵逼
問題3
public static void main(String[] args){
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.Println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a+b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));
}
答案輸出:
true
false
true
true
true
false
問題4:
執行時常量池與字串常量池是什麼關係?包含?
在解決問題之前,我們先來簡單瞭解一些常量池的一些知識點(大部分來源於周志明的深入Java虛擬機器這本書)。
JVM中的幾種常量池
1.class檔案常量池
在Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用。
這裡簡單解釋下字面量和符號引用
字面量
字面量類似與我們平常說的常量,主要包括:
- 文字字串:就是我們在程式碼中能夠看到的字串,例如String a = “aa”。其中”aa”就是字面量。
- 被final修飾的變數。
符號引用
主要包括以下常量:
- 類和介面和全限定名:例如對於String這個類,它的全限定名就是java/lang/String。
- 欄位的名稱和描述符:所謂欄位就是類或者介面中宣告的變數,包括類級別變數(static)和例項級的變數。
- 方法的名稱和描述符。所謂描述符就相當於方法的引數型別+返回值型別。
2.執行時常量池
我們上面說的class檔案中的常量池,它會在類載入後進入方法區中的執行時常量池。並且需要的注意的是,執行時常量池是全域性共享的,多個類共用一個執行時常量池。並且class檔案中常量池多個相同的字串在執行時常量池只會存在一份。
注意執行時常量池存在於方法區中。
3.字串常量池
看名字我們就可以知道字串常量池會用來存放字串,也就是說常量池中的文字字串會在類載入時進入字串常量池。
那字串常量池和執行時常量池是什麼關係呢?上面我們說常量池中的字面量會在類載入後進入執行時常量池,其中字面量中有包括文字字串,顯然從這段文字我們可以知道字串常量池存在於執行時常量池中。也就存在於方法區中。
不過在周志明那本深入java虛擬機器中有說到,到了JDK1.7時,字串常量池就被移出了方法區,轉移到了堆裡了。
那麼我們可以推斷,到了JDK1.7以及之後的版本中,執行時常量池並沒有包含字串常量池,執行時常量池存在於方法區中,而字串常量池存在於堆中。
說了這麼多,現在我們開始來解決上面提出了問題。
解決問題
問題1:
public static void main(String[] args){
String t1 = new String("1");
t1.intern();
String t2 = "1";
System.out.println(t1 == t2);
String t3 = new String("2") + new String("2");
t3.intern();
String t4 = "22";
System.out.println(t3 == t4);
}
答案輸出:
JDK1.6是 false false。
JDK1.7是 false true;
在解決這個問題之前,我們先來看另外一道面試中經常會問到的問題。
String t = new String("tt");
假如程式中只有這樣一行程式碼,那麼這行程式碼建立了幾個物件?
我們上面說過,”tt”屬於字面量,那麼它會在類載入之後存在於字串常量池中,也就是說,在 String t = new String(“tt”)這句程式碼執行之前,字串常量池就已經建立了”tt”這個字串物件了,我們都知道,new這個關鍵字會在堆中建立一個物件。
所以,這段程式碼建立了兩個物件。一個在堆中,一個在字串常量池中。
那麼下面這段程式碼又是建立了幾個物件呢?
String t1 = new String("tt");
String t2 = new String("tt");
答是這段程式碼建立了三個物件,我們上面說了,字串常量池只會儲存一份內容相同的字串。也就是說,在這兩句程式碼執行之前,字串常量池就已經建立了內容為”tt”的物件了。這兩句程式碼執行之後,又在堆中建立了兩個,所以一共建立了三個。
那麼下面這段程式碼又是建立了幾個物件?
String t = "tt";
答是1個,在這段程式碼執行之前,字串常量池已經建立了一個”tt”的物件,但由於這行程式碼並非用new的方法,所以虛擬機器會在字串常量池中尋找是否有內容為”tt”的字串物件,如果有,則直接返回這個字串的引用,所以最終結果只建立了一個物件。
回到我們的問題,在這裡我們先解釋下String 的intern方法。
例如我們呼叫了t.intern()。
在JDK1.6的時候,呼叫了這個方法之後,虛擬機器會在字串常量池在查詢是否有內容與”tt”相等的物件,如果有,則返回這個物件,如果沒有,則會在字串常量池中新增這個物件。注意,是把這個物件新增到字串常量池。
到了JDK1.7之後,如果呼叫了intern這個方法,虛擬機器會在字串常量池在查詢是否有內容與”tt”相等的物件,如果有,則返回這個物件,如果沒有。則會在堆中把這個物件的引用複製新增到字串常量池中。注意,這個時候新增的是物件在堆中的引用。
現在開始來分析問題中的程式碼
t1 = new String(“1”)。
這句程式碼執行之前,字串常量池中已經有”t”這個物件,執行之後會在堆中也建立一個”t”的物件,此時t1指向的是堆中的物件。
t1.intern();
這句程式碼執行之後,會在字串常量池尋早內容為”t”的物件,字串常量池已經存在這個物件了,把這個物件返回(不過返回之後並沒有變數來接收)。
t2 = “1”。
這句執行後會在字串常量池查詢內容為”t”的物件,字串常量池已經有這個物件了,返回給t2,此時t2指向的是常量池中的物件。
一個是常量池中的物件,一個是在堆中的物件,兩者能相等嗎?因此
t1 與 t2不相等。
接著下面
t3 = new String(“2”) + new String(“2”);
這段程式碼呼叫之前,字串常量池有一個”2″的物件,執行之後,實際上會呼叫StringBuilder的append()方法類進行拼接,最後在堆中建立一個”22″的物件,注意,此時字面量並沒有”22″這個字串,也就是說在字串常量池並沒有”22″這個物件。此時t3指向堆中”22″這個物件
t3.intern();
執行這個方法之後
在JDK1.6的時候,它在字串常量池中並沒有找到內容為”22″的物件,所以這個時候會把這個物件新增到字串常量池,並把這個物件返回(此時並沒有變數來接收這個返回的物件)。注意新增的是物件,而並非引用。
t4 = “22”。
這句程式碼執行後,會返回字串常量池中內容為”22″物件,此時t4指向的是字串常量池中的物件。
顯然,一個物件在字串常量池,一個在堆中,兩個物件並非是同一個物件,因此在JDK1.6的時候,t3與t4不相等。
但是在JDK1.7的時候
t3.intern()執行之後,由於在字串常量池在並沒有內容為”22″的物件,所以會把堆中該物件的引用賦值到字串常量池。注意此時字串常量池儲存的是堆中這個物件的引用。
t4 = “22”。
執行這句程式碼之後,從字串常量池返回給t4的是堆中物件的引用。此時t4指向的實際上是堆中物件的引用,也就是說,t3和t4指向的是同一個物件。
因此t3與t4相等。
不知道你明白了沒有?反正我是搞了好久才明白…
問題2
至於問題2,我就只講下半部分的程式碼,上半部分如果你看懂了問題1,那麼問題2也差不多自然懂了。
String t3 = new String("2") + new String("2");
String t4 = "22";
t3.intern();
System.out.println(t3 == t4);
t3 = new String(“2”) + new String(“2”)。
這段程式碼呼叫之前,字串常量池有一個”2″的物件,執行之後,實際上會呼叫StringBuilder的append()方法類進行拼接,最後在堆中建立一個”22″的物件。此時t3指向堆中”22″這個物件
t4 = “22”。
這句程式碼執行之前,字串常量池已經存在”22″這個物件了,所有直接把這個物件返回給t4,此時t4指向的是字串常量池中的物件.
所以t3和t4肯定不是同一個物件啊,t3.intern這句幾乎可以忽略,不會給t3和t4造成任何影響。
問題3
public static void main(String[] args){
Integer a = 1;
Integer b = 2;
Integer c = 3;
Integer d = 3;
Integer e = 321;
Integer f = 321;
Long g = 3L;
System.out.println(c == d);
System.out.Println(e == f);
System.out.println(c == (a + b));
System.out.println(c.equals(a+b));
System.out.println(g == (a + b));
System.out.println(g.equals(a + b));
}
對於這個問題,我簡單說一下可能你就懂了。
(1). 記憶體中有一個java基本型別封裝類的常量池。這些類包括
Byte, Short, Integer, Long, Character, Boolean。需要注意的是,Float和Double這兩個類並沒有對應的常量池。
(2).上面5種整型的包裝類也只是在物件數值在-128~127才可以使用這些常量池。
(3). 在周志明的那本虛擬機器中有這樣一句話:包裝類的
“==”執行符在不遇到算術運算的情況下不會自動拆箱,以及他們的equals()方法不處理資料型別的關係,可以推斷出如果遇到“==”兩邊有算術運算是話就會自動拆箱和進行資料型別轉換處理。
(4).Long的equals方法會先判斷是否是Long型別。
(5).無論是Integer還是Long,他們的equals方法比較的是數值。
所以:
System.out.println(c == d)。
由於常量池的作用,c與d指向的是同一個物件(注意此時的==比較的是物件,也就是地址,而不是數值)。因此為true
System.out.println(e == f)。
由於321超過了127,因此常量池失去了作用,所以e和f數值雖然相同,但不是同一個物件,以此為false。
System.out.println(c == (a+b))。
此時==兩邊有算術運算,會進行拆箱,因此此時比較的是數值,而並非物件。因此為true。
System.out.println(c.equals(a+b))
c與a+b的數值相等,為true。
System.out.pirnln(g == (a + b))
由於==兩邊有算術運算,所以比較的是數值,因此為true。
System.out.println(g.equals(a+b))。
Long型別的equal在比較是時候,會先判斷a+b是否為Long型別,顯然a+b不是,因此false
問題到此就結束了,以上便是自己的理解,以上如果有不對勁的地方,非常歡迎你的指點。
完。
關注公眾號「苦逼的碼農」,獲取更多原創文章,後臺回覆「禮包」送你一份特別的大禮包