真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

Java中String的intern()方法

這篇文章主要介紹Java中String的intern()方法,文中介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們一定要看完!

創(chuàng)新互聯(lián)成都企業(yè)網(wǎng)站建設(shè)服務(wù),提供成都網(wǎng)站制作、成都網(wǎng)站設(shè)計網(wǎng)站開發(fā),網(wǎng)站定制,建網(wǎng)站,網(wǎng)站搭建,網(wǎng)站設(shè)計,成都響應(yīng)式網(wǎng)站建設(shè),網(wǎng)頁設(shè)計師打造企業(yè)風格網(wǎng)站,提供周到的售前咨詢和貼心的售后服務(wù)。歡迎咨詢做網(wǎng)站需要多少錢:18980820575

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

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

在常量池部分我們了解了三種常量池,分別為:字符串常量池、Class文件常量池以及運行時常量池。而字符串的內(nèi)存分配則和字符串常量池有著莫大的關(guān)系。

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

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

這兩種方式在內(nèi)存分配上有什么不同呢? 相信大家在初學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)存分配情況,如下圖所示:

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

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

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

當調(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)存分配之后,我們需要再來認識一下String中一個很重要的方法:String.intern()。

很多讀者可能對于這一方法并不是太了解,但并不代表他不重要。我們先來看一下intern()方法的源碼:

/**
     * Returns a canonical representation for the string object.
     * 

* A pool of strings, initially empty, is maintained privately by the * class {@code String}. *

* 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. *

* 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}. *

* All literal strings and string-valued constant expressions are * interned. String literals are defined in section 3.10.5 of the * The Java™ Language Specification. * * @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)系,即使看不到源碼我們也能從其注釋中得到一些信息:當調(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é)中我們通過一個例子認識了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()方法有了較深的認識。那么intern()方法具體在開發(fā)中有什么用呢?推薦大家可以看下美團技術(shù)團隊的一篇文章《深入解析String#intern》中舉的兩個例子。限于篇幅,本文不再舉例分析。

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

前兩節(jié)我們認識了String的內(nèi)存分配以及它的intern()方法,這兩節(jié)內(nèi)容其實都是對String內(nèi)存的分析。到目前為止,我們還并未認識String類的結(jié)構(gòu)以及它的一些特性。那么本節(jié)內(nèi)容我們就此來分析。先通過一段代碼來大致了解一下String類的結(jié)構(gòu)(代碼取自jdk8):

public final class String
    implements java.io.Serializable, Comparable, 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)化,這就導致了不同jdk版本上String類的實現(xiàn)有些許差異,只是我們使用上并無感知。下圖列出了jdk6-jdk9中String源碼的一些變化。

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

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

從Java9開始,String中的char數(shù)組被byte[]數(shù)組所替代。我們知道一個char類型占用兩個字節(jié),而byte占用一個字節(jié)。因此在存儲單字節(jié)的String時,使用char數(shù)組會比byte數(shù)組少一個字節(jié),但本質(zhì)上并無任何差別。 另外,注意到在Java9的版本中多了一個coder,它是編碼格式的標識,在計算字符串長度或者調(diào)用 indexOf() 函數(shù)時,需要根據(jù)這個字段,判斷如何計算字符串長度。coder 屬性默認有 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

使用String的concat()方法拼接

    public class Test {        private static final int COUNT=50000;        public static void main(String[] args) {
            String str="";            for(int i=0;i

使用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

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

拼接方法最小用時(ms)最大用時(ms)平均用時(ms)
"+"操作符486851464924
String的concat方法222724562296
StringBuilder的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

把上邊這段代碼編譯后,執(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

可見,優(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對象。

以上是Java中String的intern()方法的所有內(nèi)容,感謝各位的閱讀!希望分享的內(nèi)容對大家有幫助,更多相關(guān)知識,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!


新聞名稱:Java中String的intern()方法
網(wǎng)站地址:http://weahome.cn/article/joicoc.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部