詳細(xì)圖解Java中字符串的初始化
前言
在深入學(xué)習(xí)字符串類之前,我們先搞懂JVM是怎樣處理新生字符串的。當(dāng)你知道字符串的初始化細(xì)節(jié)后,再去寫String s = "hello"
或String s = new String("hello")
等代碼時(shí),就能做到心中有數(shù)。
首先得搞懂字符串常量池的概念,下面進(jìn)入正文吧。
常量池
把經(jīng)常用到的數(shù)據(jù)存放在某塊內(nèi)存中,避免頻繁的數(shù)據(jù)創(chuàng)建與銷毀,實(shí)現(xiàn)數(shù)據(jù)共享,提高系統(tǒng)性能。
八種基礎(chǔ)數(shù)據(jù)類型除了float和double都實(shí)現(xiàn)了常量池技術(shù)。在近代的JDK版本中(1.7后),字符串常量池被實(shí)現(xiàn)在Java堆內(nèi)存中。
下面通過(guò)三行代碼讓大家對(duì)字符串常量池建立初步認(rèn)識(shí):
public static void main(String[] args) { String s1 = "hello"; String s2 = new String("hello"); System.out.println(s1 == s2); //false }
先來(lái)看看第一行代碼String s1 = "hello";
直接通過(guò)雙引號(hào)( String s1 = "hello")聲明字符串的方式,虛擬機(jī)首先會(huì)到字符串常量池中查找該字符串是否已經(jīng)存在。如果存在會(huì)直接返回該引用,如果不存在則會(huì)在堆內(nèi)存中創(chuàng)建該字符串對(duì)象,然后到字符串常量池中注冊(cè)該字符串。
上面的代碼中( String s1 = "hello")虛擬機(jī)首先會(huì)到字符串常量池中查找是否有存在hello字符串對(duì)應(yīng)的引用。發(fā)現(xiàn)沒(méi)有后會(huì)在堆內(nèi)存創(chuàng)建hello字符串對(duì)象(內(nèi)存地址0x0001),然后到字符串常量池中注冊(cè)地址為0x0001的hello對(duì)象,也就是添加指向0x0001的引用。最后把字符串對(duì)象返回給s1。
下面看String s2 = new String("hello");
當(dāng)我們使用new關(guān)鍵字創(chuàng)建字符串對(duì)象的時(shí)候,JVM將不會(huì)查詢字符串常量池,它將會(huì)直接在堆內(nèi)存中創(chuàng)建一個(gè)字符串對(duì)象,并返回給所屬變量。
所以s1和s2指向的是兩個(gè)完全不同的對(duì)象,判斷s1 == s2的時(shí)候會(huì)返回false。
再來(lái)看下面的示例:
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");
的執(zhí)行過(guò)程是這樣子的:
- 依次在堆內(nèi)存中創(chuàng)建hello和world兩個(gè)字符串對(duì)象;
- 然后把它們拼接起來(lái) (底層使用StringBuilder實(shí)現(xiàn));
- 在拼接完成后會(huì)產(chǎn)生新的hello world對(duì)象,這時(shí)變量s1指向新對(duì)象hello world。
執(zhí)行完第一行代碼后,內(nèi)存是這樣子的:
第二行代碼s1.intern();
當(dāng)調(diào)用intern()方法時(shí),首先會(huì)去常量池中查找是否有該字符串對(duì)應(yīng)的引用,如果有就直接返回該字符串;
如果沒(méi)有,就會(huì)在常量池中注冊(cè)該字符串的引用,然后返回該字符串。
由于第一行代碼采用的是new的方式創(chuàng)建字符串,所以在字符串常量池中沒(méi)有保存hello world對(duì)應(yīng)的引用,虛擬機(jī)會(huì)在常量池中進(jìn)行注冊(cè),注冊(cè)完后的內(nèi)存示意圖如下:
第三行代碼String s2 = "hello world";
首先虛擬機(jī)會(huì)去檢查字符串常量池,發(fā)現(xiàn)有指向hello world的引用。然后把該引用所指向的字符串直接返回給所屬變量。
執(zhí)行完第三行代碼后,內(nèi)存示意圖如下:
如圖所示,s1和s2指向的是相同的對(duì)象,所以當(dāng)判斷s1 == s2時(shí)返回true。
總結(jié):
- 當(dāng)用new關(guān)鍵字創(chuàng)建字符串對(duì)象時(shí),不會(huì)查詢字符串常量池;
- 當(dāng)用雙引號(hào)直接聲明字符串對(duì)象時(shí),虛擬機(jī)將會(huì)查詢字符串常量池。
說(shuō)白了就是:字符串常量池提供了字符串的復(fù)用功能,除非我們要顯式創(chuàng)建新的字符串對(duì)象,否則對(duì)同一個(gè)字符串虛擬機(jī)只會(huì)維護(hù)一份拷貝。
反編譯代碼驗(yàn)證字符串初始化操作
下面我們?cè)賮?lái)看一個(gè)示例:
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); } }
首先第一行和第二行是常規(guī)的字符串對(duì)象聲明,它們分別會(huì)在堆內(nèi)存創(chuàng)建字符串對(duì)象,并會(huì)在字符串常量池中進(jìn)行注冊(cè)。
影響我們做出判斷的是第三行代碼String s3 = s1 + s2;,我們不知道s1 + s2在創(chuàng)建完新字符串hello world后是否會(huì)在字符串常量池進(jìn)行注冊(cè)。
簡(jiǎn)單點(diǎn)說(shuō):我們不知道這行代碼是以雙引號(hào)形式聲明字符串,還是用new關(guān)鍵字創(chuàng)建字符串。
那么我們看下這端代碼的反編譯后的代碼:
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 }
直接看重點(diǎn):
- 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
- 24: astore_3
- 虛擬機(jī)調(diào)用StringBuilder的toString()方法獲得字符串hello world,并存放至s3。
下面是我們追蹤StringBuilder的toString()方法源碼:
@Override public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
通過(guò)以上源碼可以看出:s3是通過(guò)new關(guān)鍵字獲得字符串對(duì)象的。
回到題目,也就是說(shuō)字符串常量表中沒(méi)有存儲(chǔ)hello world的引用,當(dāng)s4以引號(hào)的形式聲明字符串時(shí),由于在字符串常量池中查不到相應(yīng)的引用,所以會(huì)在堆內(nèi)存中新創(chuàng)建一個(gè)字符串對(duì)象。 所以s3和s4指向的不是同一個(gè)字符串對(duì)象, 結(jié)果為false。
總結(jié)
到此這篇關(guān)于Java中字符串初始化的文章就介紹到這了,更多相關(guān)Java字符串的初始化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java 二進(jìn)制數(shù)據(jù)與16進(jìn)制字符串相互轉(zhuǎn)化方法
今天小編就為大家分享一篇java 二進(jìn)制數(shù)據(jù)與16進(jìn)制字符串相互轉(zhuǎn)化方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-07-07在jmeter的beanshell中用java獲取系統(tǒng)當(dāng)前時(shí)間的簡(jiǎn)單實(shí)例
這篇文章介紹了在jmeter的beanshell中用java獲取系統(tǒng)當(dāng)前時(shí)間的簡(jiǎn)單實(shí)例,有需要的朋友可以參考一下2013-09-09SpringBoot 在測(cè)試時(shí)如何指定包的掃描范圍
這篇文章主要介紹了SpringBoot 在測(cè)試時(shí)如何指定包的掃描范圍,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Spring Boot Actuator監(jiān)控器配置及使用解析
這篇文章主要介紹了Spring Boot Actuator監(jiān)控器配置及使用解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07web容器中實(shí)例化spring相關(guān)配置解析
這篇文章主要介紹了web容器中實(shí)例化spring相關(guān)配置解析,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01java靜態(tài)工具類注入service出現(xiàn)NullPointerException異常處理
如果我們要在我們自己封裝的Utils工具類中或者非controller普通類中使用@Autowired注解注入Service或者M(jìn)apper接口,直接注入是報(bào)錯(cuò)的,因Utils用了靜態(tài)方法,我們無(wú)法直接用非靜態(tài)接口的,遇到這問(wèn)題,我們要想法解決,下面小編就簡(jiǎn)單介紹解決辦法,需要的朋友可參考下2021-09-09Kotlin 基礎(chǔ)教程之?dāng)?shù)組容器
這篇文章主要介紹了Kotlin 基礎(chǔ)教程之?dāng)?shù)組容器的相關(guān)資料,需要的朋友可以參考下2017-06-06