久久久久久久视色,久久电影免费精品,中文亚洲欧美乱码在线观看,在线免费播放AV片

<center id="vfaef"><input id="vfaef"><table id="vfaef"></table></input></center>

    <p id="vfaef"><kbd id="vfaef"></kbd></p>

    
    
    <pre id="vfaef"><u id="vfaef"></u></pre>

      <thead id="vfaef"><input id="vfaef"></input></thead>

    1. 站長(zhǎng)資訊網(wǎng)
      最全最豐富的資訊網(wǎng)站

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

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

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

      初學(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它是數(shù)據(jù)類型中的一朵奇葩。那么,本篇文章我們就來深入的認(rèn)識(shí)一下Java中的String字符串。

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

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

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

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

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

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

      這樣的回答正確嗎?至少在現(xiàn)在看來并不完全正確,因?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")為例來分析:

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

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

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

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

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

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

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

      當(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ì)者們開辟了一塊字符串常量池區(qū)域,這塊區(qū)域是是所有類共享的,每個(gè)虛擬機(jī)只有一個(gè)字符串常量池。因此,在使用字面量方式賦值的時(shí)候,如果字符串常量池中已經(jīng)有了該字符串,則不會(huì)在堆內(nèi)存中重新創(chuàng)建對(duì)象,而是直接將其指向了字符串常量池中的對(duì)象。

      二、String的intern()方法

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

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

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

      了解了intern方法的用途之后,來看一個(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)行之后如下:

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

      如果理解了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ì)通過new來實(shí)例化一個(gè)String對(duì)象,并將該對(duì)象的引用賦值給str2,然后檢測(cè)字符串常量池中是否已經(jīng)有了與“hello world”相等的對(duì)象,如果沒有,則會(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é)果與上一致,至于原因讀者可以自行分析。

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

      2.改造例子,再看intern

      上一節(jié)中我們通過一個(gè)例子認(rèn)識(shí)了intern()方法的作用,接下來,我們對(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é)果?

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

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

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

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

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

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

      String str2=str1.intern(); 這行代碼會(huì)首先檢測(cè)字符串常量池,發(fā)現(xiàn)此時(shí)還沒有存在與”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é)果。

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

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

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

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

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

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

      String str2=str1.intern(); 這行代碼檢測(cè)到字符串常量池中還沒有“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é)果。

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

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

      前兩節(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)容我們就此來分析。先通過一段代碼來大致了解一下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ù)組,說明String是通過char數(shù)組來實(shí)現(xiàn)的,同時(shí)我們注意到這個(gè)char數(shù)組也被聲明為了final,這也是我們常說的String是不可變的原因。

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

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

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

      可以看到在Java6之前String中維護(hù)了一個(gè)char 數(shù)組、一個(gè)偏移量 offset、一個(gè)字符數(shù)量 count以及一個(gè)哈希值 hash。 String對(duì)象是通過 offset 和 count 兩個(gè)屬性來定位 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)存泄漏問題。

      從Java9開始,String中的char數(shù)組被byte[]數(shù)組所替代。我們知道一個(gè)char類型占用兩個(gè)字節(jié),而byte占用一個(gè)字節(jié)。因此在存儲(chǔ)單字節(jié)的String時(shí),使用char數(shù)組會(huì)比byte數(shù)組少一個(gè)字節(jié),但本質(zhì)上并無任何差別。 另外,注意到在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。

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

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

      (1)String的substring實(shí)現(xià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í)字符串的裁剪是通過實(shí)例化了一個(gè)新的String對(duì)象來實(shí)現(xiàn)的。所以,如果在項(xiàng)目中存在大量的字符串裁剪的代碼應(yīng)盡量避免使用String,而是使用性能更好的StringBuilder或StringBuffer來處理。

      (2)String的字符串拼接實(shí)現(xiàn)

      1)字符串拼接方案性能對(duì)比

      關(guān)于字符串的拼接有很多實(shí)現(xiàn)方法,在這里我們舉三個(gè)例子來進(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ù)制代碼

      如上代碼,通過三種方法分別進(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。這也是為什么在開發(fā)中不建議使用”+“操作符進(jìn)行字符串拼接的原因。

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

      ”+“操作符的實(shí)現(xiàn)原理由于”+“操作符是由JVM來完成的,我么無法直接看到代碼實(shí)現(xiàn)。不過Java為我們提供了一個(gè)javap的工具,可以幫助我們將Class文件進(jìn)行一個(gè)反匯編,通過匯編指令,大致可以看出”+“操作符的實(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é)果:

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

      注意圖中的”11:“行指令處實(shí)例化了一個(gè)StringBuilder,在"19:"行處調(diào)用了StringBuilder的append方法,并在第”27:"行處調(diào)用了String的toString()方法??梢?,JVM在進(jìn)行”+“字符串拼接時(shí)也是用了StringBuilder來實(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ù)制代碼

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

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

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

      四、總結(jié)

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

      贊(0)
      分享到: 更多 (0)
      網(wǎng)站地圖   滬ICP備18035694號(hào)-2    滬公網(wǎng)安備31011702889846號(hào)