【Java基礎(chǔ)概念】String相關(guān)知識

本文源碼基于JDK1.8

概述

String并不屬于Java八大基礎(chǔ)類型中的一種,但是其使用頻率卻不比任何一種基礎(chǔ)類型低,所以了解String的常用方法和一些相關(guān)類就顯得尤為重要了,否則在日常使用的過程中,就會埋下各種坑而不自知,在項目上生產(chǎn)后查找相關(guān)問題又變得極其艱難。

JVM相關(guān)

深入了解String之前,先簡單復(fù)習(xí)下JVM的相關(guān)知識,下面是Java虛擬機運行時的數(shù)據(jù)區(qū)圖示

image

程序計數(shù)器(線程私有)

程序計數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令的,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都要依賴計數(shù)器完成。

Java虛擬機棧(線程私有)

虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。虛擬機棧的生命周期與線程相同,每一個方法從調(diào)用到執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。

本地方法棧(線程私有)

本地方法棧與虛擬機棧作用類似,他們之間的區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機使用的native方法服務(wù)。

Java堆(線程共享)

Java堆(Java Heap)是Java虛擬機所管理的內(nèi)存中最大的一塊,在虛擬機啟動時創(chuàng)建。此區(qū)域的唯一目的就是存放對象的實例,幾乎所有的對象實例都在這里分配內(nèi)存。

方法區(qū)(線程共享)

方法區(qū)用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。

運行時常量池(線程共享)

運行時常量池是方法區(qū)的一部分。class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。

下面重點了解下字符串常量池

字符串常量池存放在運行時常量池中 ,字符串常量池的存在使虛擬機減少了內(nèi)存開銷,提高了性能。

當(dāng)我們使用字面量創(chuàng)建字符創(chuàng)常量時,例如Sting a = "aaa",JVM會首先檢索字符串常量池,如果該字符串已經(jīng)存在常量池中,那么直接將此字符串對象的引用地址賦值給a,引用a存放在Java虛擬機棧中。如果在常量池中沒有,那么就會實例化該字符串,并存放在常量池中,并將此字符串對象的地址賦值給a。

當(dāng)我們使用new關(guān)鍵字創(chuàng)建字符串對象時,例如String a = new String("aaa"),JVM會首先檢索字符串常量池,如果在常量池中已經(jīng)存在,那么不會在常量池中再創(chuàng)建對象,而是直接在堆中復(fù)制該對象的副本,然后將堆中對象的引用地址賦值給a ,如果常量池中不存在,那么實例化該對象并存放到常量池中,然后在堆中賦值該對象的副本,并將堆中對象的引用地址賦值給a。

String源碼分析

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

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;

    /**
     * Class String is special cased within the Serialization Stream Protocol.
     *
     * A String instance is written into an ObjectOutputStream according to
     * <a href="{@docRoot}/../platform/serialization/spec/output.html">
     * Object Serialization Specification, Section 6.2, "Stream Elements"</a>
     */
    private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
    }

上面是部分String的源碼,String類有final關(guān)鍵字修飾,這意味著String不能被繼承,并且其所有成員變量和方法都默認final修飾。String實現(xiàn)了Serializable,Comparable,CharSequence接口。Serializable用于支持序列化, Comparable用于對兩個實例化對象進行比較。CharSequence是一個只讀的字符序列,包括length(), charAt(int index), subSequence(int start, int end)這幾個API接口,值得一提的是,StringBuffer和StringBuild也是實現(xiàn)了改接口。

String成員變量

char value[]:保存String內(nèi)容的數(shù)組
offset:保存第一個索引
count:保存字符串長度
hash:緩存實例化對象時計算的hashcode值,由于String是不可變的,所以每次的hashcode必定相同,緩存之后就不需要去計算了,這樣可以提升性能

String在JVM層解析

1、創(chuàng)建字符串形式

創(chuàng)建字符串的兩種基本形式如下:

   String s1 = "1";
   String s2 = new String("1");

在虛擬機中,s1使用引號(字面量)創(chuàng)建字符串,在編譯期的時候就會對常量池檢索,判斷是否存在該字符串。如果存在那么不會創(chuàng)建新的,直接返回對象的引用;如果不存在,那么現(xiàn)在常量池中創(chuàng)建對象,然后返回對象的引用給s1。

s2使用關(guān)鍵詞new創(chuàng)建字符串,JVM會首先檢查字符串常量池,如果該字符串已經(jīng)存在常量池中,那么不再在字符串常量池創(chuàng)建該字符串對象,而直接堆中復(fù)制該對象的副本,然后將堆中對象的地址賦值給引用s2,如果字符串不存在常量池中,就會實例化該字符串并且將其放到常量池中,然后在堆中復(fù)制該對象的副本,然后將堆中對象的地址賦值給引用s2。注意:此時是運行期,那么字符串常量池是在運行時常量池中的。

2、“+”形式創(chuàng)建字符串

形式一:String s = "a" + "b" + "c"

如果使用“+”拼接的字符串全部都是常量,那么在編譯期就能確定最終的值,就可以直接入字符串常量池,一樣會先判斷靜態(tài)常量池(編譯期常量池)字符串常量池中是否存在該字符串,如果存在,則直接返回常量池中對象的引用,如果不存在,則先創(chuàng)建對象到常量池,再返回引用給s.

形式二:String s = "a" + new String("b") + "c" + "d"

如果使用“+”拼接的字符串中含有變量時,也就是說在運行期才能夠給確定具體的值。首先編譯期會將竟可能多的常量連接在一起,形成新的字符串,然后參與到后續(xù)的連接中,即
String s = "a" + new String("b") + "cd"

接下來的字符串連接是從左向右依次進行,對于不同的字符串,首先以最左邊的字符串為參數(shù)創(chuàng)建StringBuilder對象(可變字符串對象),然后依次對右邊進行append操作,最后將StringBuilder對象通過toString()方法轉(zhuǎn)換成String對象(注意:中間的多個字符串常量不會自動拼接)。實際上的實現(xiàn)過程為:String s=new StringBuilder(“a”).append(new String(“b”)).append(“cd”).toString();當(dāng)使用+進行多個字符串連接時,實際上是產(chǎn)生了一個StringBuilder對象和一個String對象。

String的Equals和“==”

String為引用數(shù)據(jù)類型,String重寫了Object的equals方法,比較的是兩個對象的值,如果值相等,則equals返回true。而“==”比較的是兩個對象的內(nèi)存地址是否相同,下面是一些String相關(guān)的equals和==比較

public  void testEquals(){
        //在編譯期,在字符串常量池中創(chuàng)建對象“abc”,返回該對象引用給s1
        String s1 = "abc";
        //在編譯期,字符串常量池中已經(jīng)存在了對象“abc”,不用重新創(chuàng)建,直接返回對象引用給s2
        String s2 = "abc";
        System.out.println("s1.equals(s2):"+ s1.equals(s2));//結(jié)果返回true,值相等
        System.out.println("s1 = s2 :" + (s1 == s2));//結(jié)果返回true,指向常量池中的同一個對象
        
        
        
        //如果不計s1的創(chuàng)建過程,那么s3創(chuàng)建時,會在常量池中創(chuàng)建一個對象,并在堆中創(chuàng)建一個對象,然后將堆中對象的引用賦值給s3
        String s3 = new String("abc");
        //s4創(chuàng)建時,由于常量池中已經(jīng)存在對象值為“abc”,所以只在堆中創(chuàng)建對象,并將對象的引用返回給s4
        String s4 = new String("abc");
        System.out.println("s3.equals(s4):"+s3.equals(s4));//結(jié)果返回true,值相等
        System.out.println("s3 = s4:"+ (s3 == s4));//返回false,指向不同的對象
        System.out.println("s3.equals(s1):"+s3.equals(s1));//返回true,值相等
        System.out.println("s3 = s1 :"+(s3 ==s1));//返回false,一個指向常量池中對象,一個指向堆中對象
        
        
        
        //由于“+”拼接的字符創(chuàng)都是常量,所以在編譯期就會被優(yōu)化, 效果等同于String s5 = "abcd";
        String s5 = "ab" + "c";
        System.out.println("s1 =s5 :" + (s1 == s5));//返回true,都指向常量池中的同一個對象
        
        
        //由于“+”拼接的字串中包含變量,所以在運行期時才能確定,并系統(tǒng)會先創(chuàng)建“ab”的StringBuilder ,然后再append
        String s6 = "ab" + new String("c");
        System.out.println("s1 = s6: " + (s1 == s6));//返回false,指向不同的對象
        
        
        String s7 = "c";
        String s8 = "ab" + s7;//由于s7是變量,在運行時才能確定,所以會產(chǎn)生新的對象,保存在堆中
        System.out.println("s1 = s8:" + (s1 == s8));//返回false,指向不同對象
        
        
        final  String s9 = "c";
        String s10 = "ab" + s9;//s9為常量,所以在編譯期就會優(yōu)化
        System.out.println("s1 = s10 :" + (s1 == s10) );//返回true,都指向常量池中的同一個對象
    }

運行結(jié)果如下:

s1.equals(s2):true
s1 = s2 :true
s3.equals(s4):true
s3 = s4:false
s3.equals(s1):true
s3 = s1 :false
s1 =s5 :true
s1 = s6: false
s1 = s8:false
s1 = s10 :true

String 、StringBuilder 與 StringBuffer 的區(qū)別

我們知道String是不可變的,當(dāng)需要拼接字符串時,會產(chǎn)生很多無用的中間對象,如果頻繁的進行這種你操作,會對性能產(chǎn)生一定的影響。而StringBuffer就是為了解決上述問題而產(chǎn)生的一個類。它提供的append和add方法可以將字符串添加到已有序列的末尾或者指定位置,其本質(zhì)上是一個線程安全的可修改的字符序列,該類把所有能修改序列的方法都加上了synchronized方法以保證線程安全,但代價就是性能比較低。

而在很多情況下,字符串拼接并不需要線程安全,所以StringBuilder就應(yīng)運而生了,StringBuilder是JDK1.5發(fā)布的,本質(zhì)上與StringBuffer沒有什么區(qū)別,只是去掉了線程安全部分,減少了開銷。

StringBuffer 和 StringBuilder 二者都繼承了 AbstractStringBuilder ,底層都是利用可修改的char數(shù)組(JDK 9 以后是 byte數(shù)組)。所以如果我們有大量的字符串拼接,如果能預(yù)知大小的話最好在new StringBuffer 或者StringBuilder 的時候設(shè)置好capacity,避免多次擴容的開銷。擴容要拋棄原有數(shù)組,還要進行數(shù)組拷貝創(chuàng)建新的數(shù)組。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 從網(wǎng)上復(fù)制的,看別人的比較全面,自己搬過來,方便以后查找。原鏈接:https://www.cnblogs.com/...
    lxtyp閱讀 1,442評論 0 9
  • 前言 RTFSC (Read the fucking source code )才是生活中最重要的。我們天天就是要...
    二毛_coder閱讀 527評論 1 1
  • String 是Java編程中的引用類型,不屬于基本類型,默認值為null,在Java中是用來創(chuàng)建于操作字符串。源...
    小杰的快樂時光閱讀 626評論 0 1
  • 所有知識點已整理成app app下載地址 J2EE 部分: 1.Switch能否用string做參數(shù)? 在 Jav...
    侯蛋蛋_閱讀 2,716評論 1 4
  • 文/覆沒 一月末的傍晚,渺茫是我的夜色 十字路口沒有路標(biāo) 只有兩三人和一圈圈孤獨的火種 我也一樣 一起忙碌在寒峭中...
    葛無疑閱讀 197評論 0 5

友情鏈接更多精彩內(nèi)容