您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)深入了解Java中字符串的用法,小編覺(jué)得挺實(shí)用的,因此分享給大家做個(gè)參考,希望大家閱讀完這篇文章后可以有所收獲。
初學(xué)Java時(shí)我們已經(jīng)知道Java中可以分為兩大數(shù)據(jù)類型,分別為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型。而在這兩大數(shù)據(jù)類型中有一個(gè)特殊的數(shù)據(jù)類型String,String屬于引用數(shù)據(jù)類型,但又有區(qū)別于其它的引用數(shù)據(jù)類型??梢哉f(shuō)它是數(shù)據(jù)類型中的一朵奇葩。那么,本篇文章我們就來(lái)深入的認(rèn)識(shí)一下Java中的String字符串。
在常量池部分我們了解了三種常量池,分別為:字符串常量池、Class文件常量池以及運(yùn)行時(shí)常量池。而字符串的內(nèi)存分配則和字符串常量池有著莫大的關(guān)系。
我們知道,實(shí)例化一個(gè)字符串可以通過(guò)兩種方法來(lái)實(shí)現(xiàn),第一種最常用的是通過(guò)字面量賦值的方式,另一種是通過(guò)構(gòu)造方法傳參的方式。代碼如下:
String str1="abc"; String str2=new String("abc");復(fù)制代碼
這兩種方式在內(nèi)存分配上有什么不同呢? 相信大家在初學(xué)Java的時(shí)候老師都有給我們講解過(guò):
1.通過(guò)字面量賦值的方式創(chuàng)建String,只會(huì)在字符串常量池中生成一個(gè)String對(duì)象。 2.通過(guò)構(gòu)造方法傳入String參數(shù)的方式會(huì)在堆內(nèi)存和字符串常量池中各生成一個(gè)String對(duì)象,并將堆內(nèi)存上String的引用放入棧。
這樣的回答正確嗎?至少在現(xiàn)在看來(lái)并不完全正確,因?yàn)樗耆Q于使用的Java版本。上一篇文章《溫故知新--你不知道的JVM內(nèi)存分配》談到HotSpot虛擬機(jī)在不同的JDK上對(duì)于字符串常量池的實(shí)現(xiàn)是不同的,摘錄如下:
在JDK7以前,字符串常量池在方法區(qū)(永久代)中,此時(shí)常量池中存放的是字符串對(duì)象。而在JDK7中,字符串常量池從方法區(qū)遷移到了堆內(nèi)存,同時(shí)將字符串對(duì)象存到了Java堆,字符串常量池中只是存入了字符串對(duì)象的引用。
這句話應(yīng)該怎么理解呢?我們以String str1=new String("abc")為例來(lái)分析:
先來(lái)分析一下JDK6的內(nèi)存分配情況,如下圖所示:
當(dāng)調(diào)用new String("abc")后,會(huì)在Java堆與常量池中各生成一個(gè)“abc”對(duì)象。同時(shí),將str1指向堆中的“abc”對(duì)象。
而在JDK7及以后版本中,由于字符串常量池被移到了堆內(nèi)存,所以內(nèi)存分配方式也有所不同,如下圖所示:
當(dāng)調(diào)用了new String("abc")后,會(huì)在堆內(nèi)存中創(chuàng)建兩個(gè)“abc"對(duì)象,str1指向其中一個(gè)”abc"對(duì)象,而常量池中則會(huì)生成一個(gè)“abc"對(duì)象的引用,并指向另一個(gè)”abc"對(duì)象。
至于Java中為什么要這么設(shè)計(jì),我們?cè)谏掀恼轮幸惨呀?jīng)解釋了: 因?yàn)镾tring是Java中使用最頻繁的一種數(shù)據(jù)類型,為了節(jié)省程序內(nèi)存提高程序性能,Java的設(shè)計(jì)者們開(kāi)辟了一塊字符串常量池區(qū)域,這塊區(qū)域是是所有類共享的,每個(gè)虛擬機(jī)只有一個(gè)字符串常量池。因此,在使用字面量方式賦值的時(shí)候,如果字符串常量池中已經(jīng)有了該字符串,則不會(huì)在堆內(nèi)存中重新創(chuàng)建對(duì)象,而是直接將其指向了字符串常量池中的對(duì)象。
在了解了String的內(nèi)存分配之后,我們需要再來(lái)認(rèn)識(shí)一下String中一個(gè)很重要的方法:String.intern()。
很多讀者可能對(duì)于這一方法并不是太了解,但并不代表他不重要。我們先來(lái)看一下intern()方法的源碼:
/** * Returns a canonical representation for the string object. * <p> * A pool of strings, initially empty, is maintained privately by the * class {@code String}. * <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> * It follows that for any two strings {@code s} and {@code t}, * {@code s.intern() == t.intern()} is {@code true} * if and only if {@code s.equals(t)} is {@code true}. * <p> * All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * <cite>The Java™ Language Specification</cite>. * * @return a string that has the same contents as this string, but is * guaranteed to be from a pool of unique strings. */ public native String intern();復(fù)制代碼
emmmmm....居然是一個(gè)native方法,不過(guò)沒(méi)關(guān)系,即使看不到源碼我們也能從其注釋中得到一些信息:當(dāng)調(diào)用intern方法的時(shí)候,如果字符串常量池中已經(jīng)包含了一個(gè)等于該String對(duì)象的字符串,則直接返回字符串常量池中該字符串的引用。否則,會(huì)將該字符串對(duì)象包含的字符串添加到常量池,并返回此對(duì)象的引用。
了解了intern方法的用途之后,來(lái)看一個(gè)簡(jiǎn)單的列子:
public class Test { public static void main(String[] args) { String str1 = "hello world"; String str2 = new String("hello world"); String str3=str2.intern(); System.out.println("str1 == str2:"+(str1 == str2)); System.out.println("str1 == str3:"+(str1 == str3)); } }復(fù)制代碼
上面的一段代碼會(huì)輸出什么?編譯運(yùn)行之后如下:
如果理解了intern方法就很容易解釋這個(gè)結(jié)果了,從上面截圖中可以看到,我們的運(yùn)行環(huán)境是JDK8。
String str1 = "hello world"; 這行代碼會(huì)首先在Java堆中創(chuàng)建一個(gè)對(duì)象,并將該對(duì)象的引用放入字符串常量池中,str1指向常量池中的引用。
String str2 = new String("hello world");這行代碼會(huì)通過(guò)new來(lái)實(shí)例化一個(gè)String對(duì)象,并將該對(duì)象的引用賦值給str2,然后檢測(cè)字符串常量池中是否已經(jīng)有了與“hello world”相等的對(duì)象,如果沒(méi)有,則會(huì)在堆內(nèi)存中再生成一個(gè)值為"hello world"的對(duì)象,并將其引用放入到字符串常量池中,否則,不會(huì)再去創(chuàng)建。這里,第一行代碼其實(shí)已經(jīng)在字符串常量池中保存了“hello world”字符串對(duì)象的引用,因此,第二行代碼就不會(huì)再次向常量池中添加“hello world"的引用。
String str3=str2.intern(); 這行代碼會(huì)首先去檢測(cè)字符串常量池中是否已經(jīng)包含了”hello world"的String對(duì)象,如果有則直接返回其引用。而在這里,str2.intern()其實(shí)剛好返回了第一行代碼中生成的“hello world"對(duì)象。
因此【System.out.println("str1 == str3:"+(str1 == str3));】這行代碼會(huì)輸出true.
如果切到JDK6,其打印結(jié)果與上一致,至于原因讀者可以自行分析。
上一節(jié)中我們通過(guò)一個(gè)例子認(rèn)識(shí)了intern()方法的作用,接下來(lái),我們對(duì)上述例子做一些修改:
public class Test { public static void main(String[] args) { String str1=new String("he")+new String("llo"); String str2=str1.intern(); String str3="hello"; System.out.println("str1 == str2:"+(str1 == str2)); System.out.println("str2 == str3:"+(str2 == str3)); } }復(fù)制代碼
先別急著看下方答案,思考一下在JDK7(或JDK7之后)及JDK6上會(huì)輸出什么結(jié)果?
我們先來(lái)看下我們先來(lái)看下JDK8的運(yùn)行結(jié)果:
通過(guò)運(yùn)行程序發(fā)現(xiàn)輸出的兩個(gè)結(jié)果都是true,這是為什么呢?我們通過(guò)一個(gè)圖來(lái)分析:
String str1=new String("he")+new String("llo"); 這行代碼中new String("he")和new String("llo")會(huì)在堆上生成四個(gè)對(duì)象,因?yàn)榕c本例無(wú)關(guān),所以圖上沒(méi)有畫(huà)出,new String("he")+new String("llo")通過(guò)”+“號(hào)拼接后最終會(huì)生成一個(gè)"hello"對(duì)象并賦值給str1。
String str2=str1.intern(); 這行代碼會(huì)首先檢測(cè)字符串常量池,發(fā)現(xiàn)此時(shí)還沒(méi)有存在與”hello"相等的字符串對(duì)象的引用,而在檢測(cè)堆內(nèi)存時(shí)發(fā)現(xiàn)堆中已經(jīng)有了“hello"對(duì)象,遂將堆中的”hello"對(duì)象的應(yīng)用放入字符串常量池中。
String str3="hello"; 這行代碼發(fā)現(xiàn)字符串常量池中已經(jīng)存在了“hello"對(duì)象的引用,因此將str3指向了字符串常量池中的引用。
此時(shí),我們發(fā)現(xiàn)str1、str2、str3指向了堆中的同一個(gè)”hello"對(duì)象,因此,就有了上邊兩個(gè)均為true的輸出結(jié)果。
我們將運(yùn)行環(huán)境切換到JDK6,來(lái)看下其輸出結(jié)果:
有點(diǎn)意思!相同的代碼在不同的JDK版本上輸出結(jié)果竟然不相等。這是怎么回事呢?我們還通過(guò)一張圖來(lái)分析:
String str1=new String("he")+new String("llo"); 這行代碼會(huì)通過(guò)new String("he")和new String("llo")會(huì)分別在Java堆與字符串常量池中各生成兩個(gè)String對(duì)象,由于與本例無(wú)關(guān),所以并沒(méi)有在圖中畫(huà)出。而new String("he")+new String("llo")通過(guò)“+”號(hào)拼接后最終會(huì)在Java堆上生成一個(gè)"hello"對(duì)象,并將其賦值給了str1。
String str2=str1.intern(); 這行代碼檢測(cè)到字符串常量池中還沒(méi)有“hello"對(duì)象,因此將堆中的”hello“對(duì)象復(fù)制到了字符串常量池,并將其賦值給str2。
String str3="hello"; 這行代碼檢測(cè)到字符串常量池中已經(jīng)有了”hello“對(duì)象,因此直接將str3指向了字符串常量池中的”hello“對(duì)象。 此時(shí)str1指向的是Java堆中的”hello“對(duì)象,而str2和str3均指向了字符串常量池中的對(duì)象。因此,有了上面的輸出結(jié)果。
通過(guò)這兩個(gè)例子,相信大家因該對(duì)String的intern()方法有了較深的認(rèn)識(shí)。那么intern()方法具體在開(kāi)發(fā)中有什么用呢?推薦大家可以看下美團(tuán)技術(shù)團(tuán)隊(duì)的一篇文章《深入解析String#intern》中舉的兩個(gè)例子。限于篇幅,本文不再舉例分析。
前兩節(jié)我們認(rèn)識(shí)了String的內(nèi)存分配以及它的intern()方法,這兩節(jié)內(nèi)容其實(shí)都是對(duì)String內(nèi)存的分析。到目前為止,我們還并未認(rèn)識(shí)String類的結(jié)構(gòu)以及它的一些特性。那么本節(jié)內(nèi)容我們就此來(lái)分析。先通過(guò)一段代碼來(lái)大致了解一下String類的結(jié)構(gòu)(代碼取自jdk8):
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 // ...}復(fù)制代碼
可以看到String類實(shí)現(xiàn)了Serializable接口、Comparable接口以及CharSequence接口,意味著它可以被序列化,同時(shí)方便我們排序。另外,String類還被聲明為了final類型,這意味著String類是不能被繼承的。而在其內(nèi)部維護(hù)了一個(gè)char數(shù)組,說(shuō)明String是通過(guò)char數(shù)組來(lái)實(shí)現(xiàn)的,同時(shí)我們注意到這個(gè)char數(shù)組也被聲明為了final,這也是我們常說(shuō)的String是不可變的原因。
Java的設(shè)計(jì)團(tuán)隊(duì)一直在對(duì)String類進(jìn)行優(yōu)化,這就導(dǎo)致了不同jdk版本上String類的實(shí)現(xiàn)有些許差異,只是我們使用上并無(wú)感知。下圖列出了jdk6-jdk9中String源碼的一些變化。
可以看到在Java6之前String中維護(hù)了一個(gè)char 數(shù)組、一個(gè)偏移量 offset、一個(gè)字符數(shù)量 count以及一個(gè)哈希值 hash。 String對(duì)象是通過(guò) offset 和 count 兩個(gè)屬性來(lái)定位 char[] 數(shù)組,獲取字符串。這么做可以高效、快速地共享數(shù)組對(duì)象,同時(shí)節(jié)省內(nèi)存空間,但這種方式很有可能會(huì)導(dǎo)致內(nèi)存泄漏。
在Java7和Java8的版本中移除了 offset 和 count 兩個(gè)變量了。這樣的好處是String對(duì)象占用的內(nèi)存稍微少了些,同時(shí) String.substring 方法也不再共享 char[],從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄漏問(wèn)題。
從Java9開(kāi)始,String中的char數(shù)組被byte[]數(shù)組所替代。我們知道一個(gè)char類型占用兩個(gè)字節(jié),而byte占用一個(gè)字節(jié)。因此在存儲(chǔ)單字節(jié)的String時(shí),使用char數(shù)組會(huì)比byte數(shù)組少一個(gè)字節(jié),但本質(zhì)上并無(wú)任何差別。 另外,注意到在Java9的版本中多了一個(gè)coder,它是編碼格式的標(biāo)識(shí),在計(jì)算字符串長(zhǎng)度或者調(diào)用 indexOf() 函數(shù)時(shí),需要根據(jù)這個(gè)字段,判斷如何計(jì)算字符串長(zhǎng)度。coder 屬性默認(rèn)有 0 和 1 兩個(gè)值, 0 代表Latin-1(單字節(jié)編碼),1 代表 UTF-16 編碼。如果 String判斷字符串只包含了 Latin-1,則 coder 屬性值為 0 ,反之則為 1。
在本節(jié)內(nèi)容的開(kāi)頭我們已經(jīng)知道了字符串的不可變性。那么為什么我們還可以使用String的substring方法進(jìn)行裁剪,甚至可以直接使用”+“連接符進(jìn)行字符串的拼接呢?
關(guān)于substring的實(shí)現(xiàn),其實(shí)我們直接深入String的源碼查看即可,源碼如下:
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }復(fù)制代碼
從這段代碼中可以看出,其實(shí)字符串的裁剪是通過(guò)實(shí)例化了一個(gè)新的String對(duì)象來(lái)實(shí)現(xiàn)的。所以,如果在項(xiàng)目中存在大量的字符串裁剪的代碼應(yīng)盡量避免使用String,而是使用性能更好的StringBuilder或StringBuffer來(lái)處理。
關(guān)于字符串的拼接有很多實(shí)現(xiàn)方法,在這里我們舉三個(gè)例子來(lái)進(jìn)行一個(gè)性能對(duì)比,分別如下:
使用”+“操作符拼接字符串
public class Test { private static final int COUNT=50000; public static void main(String[] args) { String str=""; for(int i=0;i<COUNT;i++) { str=str+"abc"; } }復(fù)制代碼
使用String的concat()方法拼接
public class Test { private static final int COUNT=50000; public static void main(String[] args) { String str=""; for(int i=0;i<COUNT;i++) { str=str+"abc"; } }復(fù)制代碼
使用StringBuilder的append方法拼接
public class Test { private static final int COUNT=50000; public static void main(String[] args) { StringBuilder str=new StringBuilder(); for(int i=0;i<COUNT;i++) { str.append("abc"); } }復(fù)制代碼
如上代碼,通過(guò)三種方法分別進(jìn)行了50000次字符串拼接,每種方法分別運(yùn)行了20次。統(tǒng)計(jì)耗時(shí),得到以下表格:
拼接方法 | 最小用時(shí)(ms) | 最大用時(shí)(ms) | 平均用時(shí)(ms) |
---|---|---|---|
"+"操作符 | 4868 | 5146 | 4924 |
String的concat方法 | 2227 | 2456 | 2296 |
StringBuilder的append方法 | 4 | 12 | 6.6 |
從以上數(shù)據(jù)中可以很直觀的看到”+“操作符的性能是最差的,平均用時(shí)達(dá)到了4924ms。其次是String的concat方法,平均用時(shí)也在2296ms。而表現(xiàn)最為優(yōu)秀的是StringBuilder的append方法,它的平均用時(shí)竟然只有6.6ms。這也是為什么在開(kāi)發(fā)中不建議使用”+“操作符進(jìn)行字符串拼接的原因。
”+“操作符的實(shí)現(xiàn)原理由于”+“操作符是由JVM來(lái)完成的,我么無(wú)法直接看到代碼實(shí)現(xiàn)。不過(guò)Java為我們提供了一個(gè)javap的工具,可以幫助我們將Class文件進(jìn)行一個(gè)反匯編,通過(guò)匯編指令,大致可以看出”+“操作符的實(shí)現(xiàn)原理。
public class Test { private static final int COUNT=50000; public static void main(String[] args) { for(int i=0;i<COUNT;i++) { str=str+"abc"; } }復(fù)制代碼
把上邊這段代碼編譯后,執(zhí)行javap,得到如下結(jié)果:
注意圖中的”11:“行指令處實(shí)例化了一個(gè)StringBuilder,在"19:"行處調(diào)用了StringBuilder的append方法,并在第”27:"行處調(diào)用了String的toString()方法??梢?jiàn),JVM在進(jìn)行”+“字符串拼接時(shí)也是用了StringBuilder來(lái)實(shí)現(xiàn)的,但為什么與直接使用StringBuilder的差距那么大呢?其實(shí),只要我們將上邊代碼轉(zhuǎn)換成虛擬機(jī)優(yōu)化后的代碼一看便知:
public class Test { private static final int COUNT=50000; public static void main(String[] args) { String str=""; for(int i=0;i<COUNT;i++) { str=new StringBuilder(str).append("abc").toString(); } }復(fù)制代碼
可見(jiàn),優(yōu)化后的代碼雖然也是用的StringBuilder,但是StringBuilder卻是在循環(huán)中實(shí)例化的,這就意味著循環(huán)了50000次,創(chuàng)建了50000個(gè)StringBuilder對(duì)象,并且調(diào)用了50000次toString()方法。怪不得用了這么長(zhǎng)時(shí)間?。?!
String的concat方法的實(shí)現(xiàn)原理關(guān)于concat方法可以直接到String內(nèi)部查看其源碼,如下:
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); }復(fù)制代碼
可以看到,在concat方法中使用Arrays的copyOf進(jìn)行了一次數(shù)組拷貝,接下來(lái)又通過(guò)getChars方法再次進(jìn)行了數(shù)組拷貝,最后通過(guò)new實(shí)例化了String對(duì)象并返回。這也意味著每調(diào)用一次concat都會(huì)生成一個(gè)String對(duì)象,但相比”+“操作符卻省去了toString方法。因此,其性能要比”+“操作符好上不少。
至于StringBuilder其實(shí)也沒(méi)必要再去分析了,畢竟”+“操作符也是基于StringBuilder實(shí)現(xiàn)的,只不過(guò)拼接過(guò)程中”+“操作符創(chuàng)建了大量的對(duì)象。而StringBuilder拼接時(shí)僅僅創(chuàng)建了一個(gè)StringBuilder對(duì)象。
關(guān)于深入了解Java中字符串的用法就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。