java 字符串第一个字符_深入Java源码剖析之字符串常量
字符串在Java生產(chǎn)開發(fā)中的使用頻率是非常高的,可見,字符串對(duì)于我們而言非常關(guān)鍵。那么從C語言過來的同學(xué)會(huì)發(fā)現(xiàn),在C中是沒有String類型的,那么C語言要想實(shí)現(xiàn)字符串就必須使用char數(shù)組,通過一個(gè)個(gè)的字符來組拼成字符串。
Java中是如何實(shí)現(xiàn)字符串的
那其實(shí)在Java中,關(guān)于字符串的實(shí)現(xiàn),其實(shí)用的也是char數(shù)組,這可以從源碼中得到體現(xiàn)。
/** * Initializes a newly created {@code String} object so that it represents * the same sequence of characters as the argument; in other words, the * newly created string is a copy of the argument string. Unless an * explicit copy of {@code original} is needed, use of this constructor is * unnecessary since Strings are immutable. * * @param original * A {@code String} */ public String(String original) { this.value = original.value; this.hash = original.hash; }復(fù)制代碼這是String類的構(gòu)造方法,而這個(gè)value實(shí)際上就是char數(shù)組。
/** The value is used for character storage. */ private final char value[];復(fù)制代碼字符串在內(nèi)存中的保存方式
我們都知道如何去創(chuàng)建一個(gè)字符串,那么, 字符串在內(nèi)存中的保存方式是怎樣的呢?在內(nèi)存中有一個(gè)區(qū)域叫做常量池,而當(dāng)我們以這樣的方式去創(chuàng)建字符串:
String s1 = "abc";String s2 = "abc";復(fù)制代碼這個(gè)字符串就一定會(huì)被保存到常量池中。而Java虛擬機(jī)如果發(fā)現(xiàn)常量池中已經(jīng)存在需要?jiǎng)?chuàng)建的字符串中,它就不會(huì)重復(fù)創(chuàng)建,而是指向那個(gè)字符串即可。
String s1 = "abc";String s2 = "abc";System.out.println(s1 == s2);復(fù)制代碼所以上述代碼段的執(zhí)行結(jié)果一定是true。但是如果使用new關(guān)鍵字區(qū)創(chuàng)建字符串,過程就不太一樣了。比如下面的聲明:
String s3 = new String("abc");String s4 = new String("abc");復(fù)制代碼過程是這樣的:首先將abc保存在常量池中,此時(shí)并沒有引用,然后new關(guān)鍵字會(huì)去創(chuàng)建一個(gè)字符串對(duì)象,就會(huì)在堆內(nèi)存中創(chuàng)建abc,然后s3變量指向abc。當(dāng)執(zhí)行第二句聲明時(shí),因?yàn)槌A砍刂幸呀?jīng)存在abc,所以不會(huì)重復(fù)創(chuàng)建,而new關(guān)鍵字又會(huì)去堆內(nèi)存開辟空間存放abc,然后s4變量指向abc。
String s3 = new String("abc");String s4 = new String("abc");System.out.println(s3 == s4);復(fù)制代碼所以上述代碼段的執(zhí)行結(jié)果一定是false。
字符串駐留
當(dāng)相同的字符串常量被多次創(chuàng)建時(shí),注意是使用雙引號(hào)(" ")顯式聲明時(shí),字符串常量對(duì)象會(huì)被保存在常量池中,且只會(huì)創(chuàng)建一個(gè)對(duì)象,這就是字符串駐留,這個(gè)名詞的產(chǎn)生就是為了提升性能。簡(jiǎn)單提一下,字符串中有一個(gè)方法叫做intern();那么這個(gè)方法有什么作用呢? 該方法會(huì)去常量池中尋找當(dāng)前調(diào)用該方法的字符串常量,若找到,則直接返回該字符串對(duì)象,若沒有,則將當(dāng)前字符串放入常量池并返回,總之該方法一定會(huì)返回字符串。
String s3 = new String("abc");String s4 = new String("abc");System.out.println(s3.intern() == s4.intern());復(fù)制代碼所以上述代碼段的執(zhí)行結(jié)果一定是true,因?yàn)樽址v留只允許常量池中一個(gè)相同字符串的存在。
JVM內(nèi)存結(jié)構(gòu)
剛才一直在說常量池,那么常量池具體在哪呢?這就要來研究一下JVM的內(nèi)存結(jié)構(gòu)。JVM分為堆、棧、方法區(qū),棧又分為本地方法棧和Java棧。
在Java7之前常量池就放在方法區(qū)里,而從Java7開始,常量池被移到了堆。這樣說過于抽象,我們可以通過代碼來感受這一過程。
String s1 = new String("hello") + new String("world");String s2 = "helloworld";System.out.println(s1 == s2);復(fù)制代碼上述程序段的執(zhí)行結(jié)果一定是false。因?yàn)閟1變量在堆中,而s2變量在常量池中,兩者肯定不相同。那么看下面這段代碼,猜猜看結(jié)果是什么?
String s1 = new String("hello") + new String("world");System.out.println(s1.intern() == s1);復(fù)制代碼按照剛才的分析,intern()返回的一定是常量池里的字符串,而s1變量在堆中,它們肯定是不一樣的,但運(yùn)行結(jié)果竟然是true。那是不是就能解釋常量池在堆中,所以它們指向的是同一個(gè)對(duì)象呢?其實(shí)還不完全是,我們可以繼續(xù)看一段代碼。
String s1 = new String("hello") + new String("world");System.out.println(s1.intern() == s1);String s2 = new String("hello") + new String("world"); System.out.println(s2.intern() == s2);復(fù)制代碼這段代碼的運(yùn)行結(jié)果:
truefalse復(fù)制代碼感覺很神奇,讓人猜不透,摸不著。別急,下面我們來一起分析一下。
通過這個(gè)圖來理解一下,首先第一行代碼會(huì)在常量池中創(chuàng)建hello和world兩個(gè)字符串,接著在堆中開辟了一個(gè)空間存放組合后的字符串helloworld,然后變量s1指向它。我們說intern()會(huì)返回常量池中的字符串,那么在常量池中沒有helloworld的情況下intern()方法會(huì)怎樣處理呢?其實(shí)它會(huì)將對(duì)堆中helloworld的引用放入常量池中,此時(shí)s1.intern()和s1都指向的是同一個(gè)對(duì)象,它們是相等的。但是s2在創(chuàng)建的過程中也會(huì)在堆中開辟一個(gè)空間存放helloworld,使變量s2指向它,而s2.intern()方法在執(zhí)行的時(shí)候發(fā)現(xiàn),helloworld的引用已經(jīng)存在,所以直接返回,但此時(shí)返回的其實(shí)是s1變量的引用,那么s2.intern()與s2不相等相信大家能夠理解了。
String s1 = new String("hello") + new String("world");System.out.println(s1.intern() == s1);String s2 = new String("hello") + new String("world");System.out.println(s2.intern() == s1);復(fù)制代碼那么這段程序的輸出結(jié)果你若是能立馬知曉,那么恭喜你,前面的知識(shí)點(diǎn)你已基本掌握。執(zhí)行結(jié)果就是:
truetrue復(fù)制代碼我們還可以通過一個(gè)極端的方法來判斷常量池的位置。
List list = new ArrayList();String str = "boom";for(int i = 0;i < Integer.MAX_VALUE;i++) { String temp = str + i; str = temp; list.add(temp.intern()); }復(fù)制代碼通過編寫這一段程序能夠讓JVM去不停地將字符串變量存入常量池從而使其內(nèi)存溢出,內(nèi)存溢出后控制臺(tái)信息如下:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at java.util.Arrays.copyOfRange(Arrays.java:2694) at java.lang.String.(String.java:203) at java.lang.StringBuilder.toString(StringBuilder.java:405) at com.itcast.test2.StringTest.main(StringTest.java:25)復(fù)制代碼可以看到,控制臺(tái)信息提示堆內(nèi)存溢出,這也可以得出常量池的位置是在堆內(nèi)。這是Java7及其以后版本的輸出信息,當(dāng)我們將版本切換為Java7之前的版本,同樣的代碼,輸出信息如下:
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space at java.util.Arrays.copyOfRange(Arrays.java:2694) at java.lang.String.(String.java:203) at java.lang.StringBuilder.toString(StringBuilder.java:405) at com.itcast.test2.StringTest.main(StringTest.java:25)復(fù)制代碼PermGen space其實(shí)就是方法區(qū), 那么其實(shí)在JVM中的堆,一般分為三大部分:新生代、老年代、永久代:這個(gè)PermGen space就是永久代,也就是方法區(qū),叫法不同而已。
其它問題
繼續(xù)來探討一下關(guān)于字符串常量的一些其它問題。
String s1 = "hello" + "world";String s2 = "helloworld";System.out.println(s1 == s2); String temp = "hello";String s3 = temp + "world";String s4 = "helloworld";System.out.println(s3 == s4);復(fù)制代碼那么,這兩個(gè)輸出的結(jié)果是什么呢?結(jié)果是:
truefalse復(fù)制代碼第一個(gè)輸出為true不難理解,因?yàn)閟1和s2指向的都是常量池中的helloworld字符串,那么s3和s4難道就不是嗎?它還真就不是這樣了。s3在創(chuàng)建過程中會(huì)將temp保存在堆內(nèi)存中,所以s3和s4指向的對(duì)象不是同一個(gè)。我們可以通過反編譯來證實(shí),將這段代碼的.class文件進(jìn)行反編譯,結(jié)果如下:
String s1 = "helloworld";String s2 = "helloworld"; System.out.println(s1 == s2); String temp = "hello";String s3 = String.valueOf(temp) + "world";String s4 = "helloworld";System.out.println(s3 == s4);復(fù)制代碼我們可以看到,s1和s2的創(chuàng)建過程其實(shí)是一模一樣的,其實(shí),JVM為了優(yōu)化速度,當(dāng)它確定是兩個(gè)字符串常量進(jìn)行拼接時(shí),它會(huì)在編譯器就完成拼接,而并不會(huì)去創(chuàng)建對(duì)象處理,但是s3的創(chuàng)建要經(jīng)過temp變量,因?yàn)镴VM無法在編譯期就推測(cè)出temp,所以它要通過String對(duì)象來進(jìn)行處理,將temp放入堆內(nèi)存。所以,并不是說只有出現(xiàn)new關(guān)鍵字變量才會(huì)放入堆內(nèi)存中。
希望這篇文章能夠使你更加深入地理解字符串常量。
總結(jié)
以上是生活随笔為你收集整理的java 字符串第一个字符_深入Java源码剖析之字符串常量的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在Linux里awk与sed的区别,li
- 下一篇: java中for的常规用法_Java f