前言
在深入學習字符串類之前,我們先搞懂JVM是怎樣處理新生字符串的。當你知道字符串的初始化細節后,再去寫String s = "hello"
或String s = new String("hello")
等代碼時,就能做到心中有數。
首先得搞懂字符串常量池的概念,下面進入正文吧。
常量池
把經常用到的數據存放在某塊內存中,避免頻繁的數據創建與銷毀,實現數據共享,提高系統性能。
八種基礎數據類型除了float和double都實現了常量池技術。在近代的JDK版本中(1.7后),字符串常量池被實現在Java堆內存中。
下面通過三行代碼讓大家對字符串常量池建立初步認識:
1
2
3
4
5
|
public
static
void
main(String[] args) {
String s1 =
"hello"
;
String s2 =
new
String(
"hello"
);
System.out.println(s1 == s2);
//false
}
|
先來看看第一行代碼String s1 = "hello";
直接通過雙引號( String s1 = "hello")聲明字符串的方式,虛擬機首先會到字符串常量池中查找該字符串是否已經存在。如果存在會直接返回該引用,如果不存在則會在堆內存中創建該字符串對象,然后到字符串常量池中注冊該字符串。
上面的代碼中( String s1 = "hello")虛擬機首先會到字符串常量池中查找是否有存在hello字符串對應的引用。發現沒有后會在堆內存創建hello字符串對象(內存地址0x0001),然后到字符串常量池中注冊地址為0x0001的hello對象,也就是添加指向0x0001的引用。最后把字符串對象返回給s1。
下面看String s2 = new String("hello");
當我們使用new關鍵字創建字符串對象的時候,JVM將不會查詢字符串常量池,它將會直接在堆內存中創建一個字符串對象,並返回給所屬變量。
所以s1和s2指向的是兩個完全不同的對象,判斷s1 == s2的時候會返回false。
再來看下面的示例:
1
2
3
4
5
6
|
public
static
void
main(String[] args) {
String s1 =
new
String(
"hello "
) +
new
String(
"world"
);
s1.intern();
String s2 =
"hello world"
;
System.out.println(s1 == s2);
//true
}
|
第一行代碼String s1 = new String("hello ") + new String("world");
的執行過程是這樣子的:
- 依次在堆內存中創建hello和world兩個字符串對象;
- 然后把它們拼接起來 (底層使用StringBuilder實現);
- 在拼接完成后會產生新的hello world對象,這時變量s1指向新對象hello world。
執行完第一行代碼后,內存是這樣子的:
第二行代碼s1.intern();
當調用intern()方法時,首先會去常量池中查找是否有該字符串對應的引用,如果有就直接返回該字符串;
如果沒有,就會在常量池中注冊該字符串的引用,然后返回該字符串。
由於第一行代碼采用的是new的方式創建字符串,所以在字符串常量池中沒有保存hello world對應的引用,虛擬機會在常量池中進行注冊,注冊完后的內存示意圖如下:
第三行代碼String s2 = "hello world";
首先虛擬機會去檢查字符串常量池,發現有指向hello world的引用。然后把該引用所指向的字符串直接返回給所屬變量。
執行完第三行代碼后,內存示意圖如下:
如圖所示,s1和s2指向的是相同的對象,所以當判斷s1 == s2時返回true。
總結:
- 當用new關鍵字創建字符串對象時,不會查詢字符串常量池;
- 當用雙引號直接聲明字符串對象時,虛擬機將會查詢字符串常量池。
說白了就是:字符串常量池提供了字符串的復用功能,除非我們要顯式創建新的字符串對象,否則對同一個字符串虛擬機只會維護一份拷貝。
反編譯代碼驗證字符串初始化操作
下面我們再來看一個示例:
1
2
3
4
5
6
7
8
9
|
public
class
Main {
public
static
void
main(String[] args) {
String s1 =
"hello "
;
String s2 =
"world"
;
String s3 = s1 + s2;
String s4 =
"hello world"
;
System.out.println(s3 == s4);
}
}
|
首先第一行和第二行是常規的字符串對象聲明,它們分別會在堆內存創建字符串對象,並會在字符串常量池中進行注冊。
影響我們做出判斷的是第三行代碼String s3 = s1 + s2;,我們不知道s1 + s2在創建完新字符串hello world后是否會在字符串常量池進行注冊。
簡單點說:我們不知道這行代碼是以雙引號形式聲明字符串,還是用new關鍵字創建字符串。
那么我們看下這端代碼的反編譯后的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
PS D:\code\javaSE\target\classes\demo> javap -c .\Main.
class
Compiled from
"Main.java"
public
class
demo.Main {
public
demo.Main();
Code:
0
: aload_0
1
: invokespecial #
1
// Method java/lang/Object."<init>":()V
4
:
return
public
static
void
main(java.lang.String[]);
Code:
0
: ldc #
2
// String hello
2
: astore_1
3
: ldc #
3
// String world
5
: astore_2
6
:
new
#
4
// class java/lang/StringBuilder
9
: dup
10
: invokespecial #
5
// Method java/lang/StringBuilder."<init>":()V
13
: aload_1
14
: invokevirtual #
6
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
17
: aload_2
18
: invokevirtual #
6
// Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21
: invokevirtual #
7
// Method java/lang/StringBuilder.toString:()Ljava/lang/String;
24
: astore_3
25
: ldc #
8
// String hello world
27
: astore
4
29
: getstatic #
9
// Field java/lang/System.out:Ljava/io/PrintStream;
32
: aload_3
33
: aload
4
35
: if_acmpne
42
38
: iconst_1
39
:
goto
43
42
: iconst_0
43
: invokevirtual #
10
// Method java/io/PrintStream.println:(Z)V
46
:
return
}
|
直接看重點:
- 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 24: astore_3
- 虛擬機調用StringBuilder的toString()方法獲得字符串hello world,並存放至s3。
下面是我們追蹤StringBuilder的toString()方法源碼:
1
2
3
4
5
|
@Override
public
String toString() {
// Create a copy, don't share the array
return
new
String(value,
0
, count);
}
|
通過以上源碼可以看出:s3是通過new關鍵字獲得字符串對象的。
回到題目,也就是說字符串常量表中沒有存儲hello world的引用,當s4以引號的形式聲明字符串時,由於在字符串常量池中查不到相應的引用,所以會在堆內存中新創建一個字符串對象。 所以s3和s4指向的不是同一個字符串對象, 結果為false。
總結
到此這篇關於Java中字符串初始化的文章就介紹到這了,更多相關Java字符串的初始化內容請搜索陸游唐婉以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持短鏈接!