中文字幕日韩精品一区二区免费_精品一区二区三区国产精品无卡在_国精品无码专区一区二区三区_国产αv三级中文在线

溫故知新(1)深入認(rèn)識Java中的字符串

相關(guān)學(xué)習(xí)推薦:java基礎(chǔ)教程

創(chuàng)新互聯(lián)建站長期為上千多家客戶提供的網(wǎng)站建設(shè)服務(wù),團隊從業(yè)經(jīng)驗10年,關(guān)注不同地域、不同群體,并針對不同對象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺,與合作伙伴共同營造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為明水企業(yè)提供專業(yè)的成都做網(wǎng)站、成都網(wǎng)站建設(shè),明水網(wǎng)站改版等技術(shù)服務(wù)。擁有10多年豐富建站經(jīng)驗和眾多成功案例,為您定制開發(fā)。

初學(xué)Java時我們已經(jīng)知道Java中可以分為兩大數(shù)據(jù)類型,分別為基本數(shù)據(jù)類型和引用數(shù)據(jù)類型。而在這兩大數(shù)據(jù)類型中有一個特殊的數(shù)據(jù)類型String,String屬于引用數(shù)據(jù)類型,但又有區(qū)別于其它的引用數(shù)據(jù)類型??梢哉f它是數(shù)據(jù)類型中的一朵奇葩。那么,本篇文章我們就來深入的認(rèn)識一下Java中的String字符串。

一、從String字符串的內(nèi)存分配說起

上一篇文章《溫故知新--你不知道的JVM內(nèi)存分配》詳細(xì)的分析了JVM的內(nèi)存模型。在常量池部分我們了解了三種常量池,分別為:字符串常量池、Class文件常量池以及運行時常量池。而字符串的內(nèi)存分配則和字符串常量池有著莫大的關(guān)系。

我們知道,實例化一個字符串可以通過兩種方法來實現(xiàn),第一種最常用的是通過字面量賦值的方式,另一種是通過構(gòu)造方法傳參的方式。代碼如下:

String str1="abc";
    String str2=new String("abc");復(fù)制代碼

這兩種方式在內(nèi)存分配上有什么不同呢? 相信大家在初學(xué)Java的時候老師都有給我們講解過:

1.通過字面量賦值的方式創(chuàng)建String,只會在字符串常量池中生成一個String對象。 2.通過構(gòu)造方法傳入String參數(shù)的方式會在堆內(nèi)存和字符串常量池中各生成一個String對象,并將堆內(nèi)存上String的引用放入棧。

這樣的回答正確嗎?至少在現(xiàn)在看來并不完全正確,因為它完全取決于使用的Java版本。上一篇文章《溫故知新--你不知道的JVM內(nèi)存分配》談到HotSpot虛擬機在不同的JDK上對于字符串常量池的實現(xiàn)是不同的,摘錄如下:

在JDK7以前,字符串常量池在方法區(qū)(永久代)中,此時常量池中存放的是字符串對象。而在JDK7中,字符串常量池從方法區(qū)遷移到了堆內(nèi)存,同時將字符串對象存到了Java堆,字符串常量池中只是存入了字符串對象的引用。

這句話應(yīng)該怎么理解呢?我們以String str1=new String("abc")為例來分析:

1.JDK6中的內(nèi)存分配

先來分析一下JDK6的內(nèi)存分配情況,如下圖所示:

當(dāng)調(diào)用new String("abc")后,會在Java堆與常量池中各生成一個“abc”對象。同時,將str1指向堆中的“abc”對象。

2.JDK7中的內(nèi)存分配

而在JDK7及以后版本中,由于字符串常量池被移到了堆內(nèi)存,所以內(nèi)存分配方式也有所不同,如下圖所示:

當(dāng)調(diào)用了new String("abc")后,會在堆內(nèi)存中創(chuàng)建兩個“abc"對象,str1指向其中一個”abc"對象,而常量池中則會生成一個“abc"對象的引用,并指向另一個”abc"對象。

至于Java中為什么要這么設(shè)計,我們在上篇文章中也已經(jīng)解釋了: 因為String是Java中使用最頻繁的一種數(shù)據(jù)類型,為了節(jié)省程序內(nèi)存提高程序性能,Java的設(shè)計者們開辟了一塊字符串常量池區(qū)域,這塊區(qū)域是是所有類共享的,每個虛擬機只有一個字符串常量池。因此,在使用字面量方式賦值的時候,如果字符串常量池中已經(jīng)有了該字符串,則不會在堆內(nèi)存中重新創(chuàng)建對象,而是直接將其指向了字符串常量池中的對象。

二、String的intern()方法

在了解了String的內(nèi)存分配之后,我們需要再來認(rèn)識一下String中一個很重要的方法:String.intern()。

很多讀者可能對于這一方法并不是太了解,但并不代表他不重要。我們先來看一下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&trade; 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....居然是一個native方法,不過沒關(guān)系,即使看不到源碼我們也能從其注釋中得到一些信息:當(dāng)調(diào)用intern方法的時候,如果字符串常量池中已經(jīng)包含了一個等于該String對象的字符串,則直接返回字符串常量池中該字符串的引用。否則,會將該字符串對象包含的字符串添加到常量池,并返回此對象的引用。

1.一個關(guān)于intern()的簡單例子

了解了intern方法的用途之后,來看一個簡單的列子:

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ù)制代碼

上面的一段代碼會輸出什么?編譯運行之后如下:

如果理解了intern方法就很容易解釋這個結(jié)果了,從上面截圖中可以看到,我們的運行環(huán)境是JDK8。

String str1 = "hello world";這行代碼會首先在Java堆中創(chuàng)建一個對象,并將該對象的引用放入字符串常量池中,str1指向常量池中的引用。

String str2 = new String("hello world");這行代碼會通過new來實例化一個String對象,并將該對象的引用賦值給str2,然后檢測字符串常量池中是否已經(jīng)有了與“hello world”相等的對象,如果沒有,則會在堆內(nèi)存中再生成一個值為"hello world"的對象,并將其引用放入到字符串常量池中,否則,不會再去創(chuàng)建。這里,第一行代碼其實已經(jīng)在字符串常量池中保存了“hello world”字符串對象的引用,因此,第二行代碼就不會再次向常量池中添加“hello world"的引用。

String str3=str2.intern();這行代碼會首先去檢測字符串常量池中是否已經(jīng)包含了”hello world"的String對象,如果有則直接返回其引用。而在這里,str2.intern()其實剛好返回了第一行代碼中生成的“hello world"對象。

因此【System.out.println("str1 == str3:"+(str1 == str3));】這行代碼會輸出true.

如果切到JDK6,其打印結(jié)果與上一致,至于原因讀者可以自行分析。

2.改造例子,再看intern

上一節(jié)中我們通過一個例子認(rèn)識了intern()方法的作用,接下來,我們對上述例子做一些修改:

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上會輸出什么結(jié)果?

1).JDK8的運行結(jié)果分析

我們先來看下我們先來看下JDK8的運行結(jié)果:

通過運行程序發(fā)現(xiàn)輸出的兩個結(jié)果都是true,這是為什么呢?我們通過一個圖來分析:

String str1=new String("he")+new String("llo");這行代碼中new String("he")和new String("llo")會在堆上生成四個對象,因為與本例無關(guān),所以圖上沒有畫出,new String("he")+new String("llo")通過”+“號拼接后最終會生成一個"hello"對象并賦值給str1。

String str2=str1.intern();這行代碼會首先檢測字符串常量池,發(fā)現(xiàn)此時還沒有存在與”hello"相等的字符串對象的引用,而在檢測堆內(nèi)存時發(fā)現(xiàn)堆中已經(jīng)有了“hello"對象,遂將堆中的”hello"對象的應(yīng)用放入字符串常量池中。

String str3="hello";這行代碼發(fā)現(xiàn)字符串常量池中已經(jīng)存在了“hello"對象的引用,因此將str3指向了字符串常量池中的引用。

此時,我們發(fā)現(xiàn)str1、str2、str3指向了堆中的同一個”hello"對象,因此,就有了上邊兩個均為true的輸出結(jié)果。

2).JDK6的運行結(jié)果分析

我們將運行環(huán)境切換到JDK6,來看下其輸出結(jié)果:

有點意思!相同的代碼在不同的JDK版本上輸出結(jié)果竟然不相等。這是怎么回事呢?我們還通過一張圖來分析:

String str1=new String("he")+new String("llo");這行代碼會通過new String("he")和new String("llo")會分別在Java堆與字符串常量池中各生成兩個String對象,由于與本例無關(guān),所以并沒有在圖中畫出。而new String("he")+new String("llo")通過“+”號拼接后最終會在Java堆上生成一個"hello"對象,并將其賦值給了str1。

String str2=str1.intern();這行代碼檢測到字符串常量池中還沒有“hello"對象,因此將堆中的”hello“對象復(fù)制到了字符串常量池,并將其賦值給str2。

String str3="hello";這行代碼檢測到字符串常量池中已經(jīng)有了”hello“對象,因此直接將str3指向了字符串常量池中的”hello“對象。 此時str1指向的是Java堆中的”hello“對象,而str2和str3均指向了字符串常量池中的對象。因此,有了上面的輸出結(jié)果。

通過這兩個例子,相信大家因該對String的intern()方法有了較深的認(rèn)識。那么intern()方法具體在開發(fā)中有什么用呢?推薦大家可以看下美團技術(shù)團隊的一篇文章《深入解析String#intern》中舉的兩個例子。限于篇幅,本文不再舉例分析。

三、String類的結(jié)構(gòu)及特性分析

前兩節(jié)我們認(rèn)識了String的內(nèi)存分配以及它的intern()方法,這兩節(jié)內(nèi)容其實都是對String內(nèi)存的分析。到目前為止,我們還并未認(rèn)識String類的結(jié)構(gòu)以及它的一些特性。那么本節(jié)內(nè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類實現(xiàn)了Serializable接口、Comparable接口以及CharSequence接口,意味著它可以被序列化,同時方便我們排序。另外,String類還被聲明為了final類型,這意味著String類是不能被繼承的。而在其內(nèi)部維護了一個char數(shù)組,說明String是通過char數(shù)組來實現(xiàn)的,同時我們注意到這個char數(shù)組也被聲明為了final,這也是我們常說的String是不可變的原因。

1.不同JDK版本之間String的差異

Java的設(shè)計團隊一直在對String類進行優(yōu)化,這就導(dǎo)致了不同jdk版本上String類的實現(xiàn)有些許差異,只是我們使用上并無感知。下圖列出了jdk6-jdk9中String源碼的一些變化。

可以看到在Java6之前String中維護了一個char 數(shù)組、一個偏移量 offset、一個字符數(shù)量 count以及一個哈希值 hash。 String對象是通過 offset 和 count 兩個屬性來定位 char[] 數(shù)組,獲取字符串。這么做可以高效、快速地共享數(shù)組對象,同時節(jié)省內(nèi)存空間,但這種方式很有可能會導(dǎo)致內(nèi)存泄漏。

在Java7和Java8的版本中移除了 offset 和 count 兩個變量了。這樣的好處是String對象占用的內(nèi)存稍微少了些,同時 String.substring 方法也不再共享 char[],從而解決了使用該方法可能導(dǎo)致的內(nèi)存泄漏問題。

從Java9開始,String中的char數(shù)組被byte[]數(shù)組所替代。我們知道一個char類型占用兩個字節(jié),而byte占用一個字節(jié)。因此在存儲單字節(jié)的String時,使用char數(shù)組會比byte數(shù)組少一個字節(jié),但本質(zhì)上并無任何差別。 另外,注意到在Java9的版本中多了一個coder,它是編碼格式的標(biāo)識,在計算字符串長度或者調(diào)用 indexOf() 函數(shù)時,需要根據(jù)這個字段,判斷如何計算字符串長度。coder 屬性默認(rèn)有 0 和 1 兩個值, 0 代表Latin-1(單字節(jié)編碼),1 代表 UTF-16 編碼。如果 String判斷字符串只包含了 Latin-1,則 coder 屬性值為 0 ,反之則為 1。

2.String字符串的裁剪、拼接等操作分析

在本節(jié)內(nèi)容的開頭我們已經(jīng)知道了字符串的不可變性。那么為什么我們還可以使用String的substring方法進行裁剪,甚至可以直接使用”+“連接符進行字符串的拼接呢?

(1)String的substring實現(xiàn)

關(guān)于substring的實現(xiàn),其實我們直接深入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ù)制代碼

從這段代碼中可以看出,其實字符串的裁剪是通過實例化了一個新的String對象來實現(xiàn)的。所以,如果在項目中存在大量的字符串裁剪的代碼應(yīng)盡量避免使用String,而是使用性能更好的StringBuilder或StringBuffer來處理。

(2)String的字符串拼接實現(xiàn)1)字符串拼接方案性能對比

關(guān)于字符串的拼接有很多實現(xiàn)方法,在這里我們舉三個例子來進行一個性能對比,分別如下:

使用”+“操作符拼接字符串

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ù)制代碼

如上代碼,通過三種方法分別進行了50000次字符串拼接,每種方法分別運行了20次。統(tǒng)計耗時,得到以下表格:

拼接方法最小用時(ms)用時(ms)平均用時(ms)"+"操作符486851464924String的concat方法222724562296StringBuilder的append方法4126.6

從以上數(shù)據(jù)中可以很直觀的看到”+“操作符的性能是最差的,平均用時達到了4924ms。其次是String的concat方法,平均用時也在2296ms。而表現(xiàn)最為優(yōu)秀的是StringBuilder的append方法,它的平均用時竟然只有6.6ms。這也是為什么在開發(fā)中不建議使用”+“操作符進行字符串拼接的原因。

2)三種字符串拼接方案原理分析

”+“操作符的實現(xiàn)原理由于”+“操作符是由JVM來完成的,我么無法直接看到代碼實現(xiàn)。不過Java為我們提供了一個javap的工具,可以幫助我們將Class文件進行一個反匯編,通過匯編指令,大致可以看出”+“操作符的實現(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:“行指令處實例化了一個StringBuilder,在"19:"行處調(diào)用了StringBuilder的append方法,并在第”27:"行處調(diào)用了String的toString()方法??梢?,JVM在進行”+“字符串拼接時也是用了StringBuilder來實現(xiàn)的,但為什么與直接使用StringBuilder的差距那么大呢?其實,只要我們將上邊代碼轉(zhuǎn)換成虛擬機優(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ù)制代碼

可見,優(yōu)化后的代碼雖然也是用的StringBuilder,但是StringBuilder卻是在循環(huán)中實例化的,這就意味著循環(huán)了50000次,創(chuàng)建了50000個StringBuilder對象,并且調(diào)用了50000次toString()方法。怪不得用了這么長時間?。?!

String的concat方法的實現(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進行了一次數(shù)組拷貝,接下來又通過getChars方法再次進行了數(shù)組拷貝,最后通過new實例化了String對象并返回。這也意味著每調(diào)用一次concat都會生成一個String對象,但相比”+“操作符卻省去了toString方法。因此,其性能要比”+“操作符好上不少。

至于StringBuilder其實也沒必要再去分析了,畢竟”+“操作符也是基于StringBuilder實現(xiàn)的,只不過拼接過程中”+“操作符創(chuàng)建了大量的對象。而StringBuilder拼接時僅僅創(chuàng)建了一個StringBuilder對象。

四、總結(jié)

本篇文章我們深入分析了String字符串的內(nèi)存分配、intern()方法,以及String類的結(jié)構(gòu)及特性。關(guān)于這塊知識,網(wǎng)上的文章魚龍混雜,甚至眾說紛紜。筆者也是參考了大量的文章并結(jié)合自己的理解來做的分析。但是,避免不了的可能會出現(xiàn)理解偏差的問題,如果有,希望大家多多討論給予指正。 同時,文章中多次提到StringBuilder,但限于文章篇幅,沒能給出關(guān)于其詳細(xì)分析。不過不用擔(dān)心,我會在下一篇文章中再做探討。 不管怎樣,相信大家看完這篇文章后一定 對String有了更加深入的認(rèn)識,尤其是了解String類的一些裁剪及拼接中可能造成的性能問題,在今后的開發(fā)中應(yīng)該盡量避免。

當(dāng)前題目:溫故知新(1)深入認(rèn)識Java中的字符串
本文網(wǎng)址:http://www.rwnh.cn/article28/cjoecp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供關(guān)鍵詞優(yōu)化、域名注冊、商城網(wǎng)站、定制開發(fā)、虛擬主機、手機網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

綿陽服務(wù)器托管
弥勒县| 建始县| 汉川市| 长治县| 安图县| 凉山| 凭祥市| 来凤县| 抚宁县| 沾化县| 崇阳县| 安图县| 常州市| 惠州市| 澄江县| 如东县| 鄂托克旗| 家居| 巴塘县| 武威市| 浪卡子县| 磴口县| 麦盖提县| 大安市| 本溪市| 红河县| 油尖旺区| 沛县| 邢台县| 怀柔区| 云南省| 通渭县| 慈利县| 福州市| 沙雅县| 建昌县| 治县。| 阿坝县| 万宁市| 郑州市| 东乡县|