<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試常備,字符串三劍客 String、StringBuffer、StringBuilder

          共 7593字,需瀏覽 16分鐘

           ·

          2021-02-22 08:50

          字符串操作毫無(wú)疑問(wèn)是計(jì)算機(jī)程序設(shè)計(jì)中最常見(jiàn)的行為之一,在 Java 大展拳腳的 Web 系統(tǒng)中更是如此。

          全文脈絡(luò)思維導(dǎo)圖如下:

          1. 三劍客之首:不可變的 String

          概述

          「Java 沒(méi)有內(nèi)置的字符串類(lèi)型」, 而是在標(biāo)準(zhǔn) Java 類(lèi)庫(kù)中提供了一個(gè)「預(yù)定義類(lèi)」?String。每個(gè)用「雙引號(hào)括起來(lái)的字符串都是?String?類(lèi)的一個(gè)實(shí)例」

          String?e?=?"";?//?空串
          String?str?=?"hello";

          看一下?String?的源碼,「在 Java 8 中,String?內(nèi)部是使用?char?數(shù)組來(lái)存儲(chǔ)數(shù)據(jù)的」

          public?final?class?String
          ????implements?java.io.Serializable,?Comparable<String>,?CharSequence?
          {
          ????/**?The?value?is?used?for?character?storage.?*/
          ????private?final?char?value[];
          }
          ?

          可以看到,String?類(lèi)是被?final?修飾的,因此?String?類(lèi)不允許被繼承」。

          ?

          「而在?Java 9?之后,String?類(lèi)的實(shí)現(xiàn)改用?byte?數(shù)組存儲(chǔ)字符串」,同時(shí)使用?coder?來(lái)標(biāo)識(shí)使用了哪種編碼。

          public?final?class?String
          ????implements?java.io.Serializable,?Comparable<String>,?CharSequence?
          {
          ????/**?The?value?is?used?for?character?storage.?*/
          ????private?final?byte[]?value;

          ????/**?The?identifier?of?the?encoding?used?to?encode?the?bytes?in?{@code?value}.?*/
          ????private?final?byte?coder;
          }

          不過(guò),無(wú)論是 Java 8 還是 Java 9,「用來(lái)存儲(chǔ)數(shù)據(jù)的 char 或者 byte 數(shù)組?value?都一直是被聲明為?final?的」,這意味著?value?數(shù)組初始化之后就不能再引用其它數(shù)組了。并且?String內(nèi)部沒(méi)有改變?value?數(shù)組的方法,因此我們就說(shuō)?String?是不可變的。

          所謂不可變,就如同數(shù)字 3 永遠(yuǎn)是數(shù)字 3 —樣,字符串 “hello” 永遠(yuǎn)包含字符 h、e、1、1 和 o 的代碼單元序列, 不能修改其中的任何一個(gè)字符。當(dāng)然, 可以修改字符串變量 str, 讓它引用另外一個(gè)字符串, 這就如同可以將存放 3 的數(shù)值變量改成存放 4 一樣。

          我們看個(gè)例子:

          String?str?=?"asdf";
          String?x?=?str.toUpperCase();

          toUpperCase?用來(lái)將字符串全部轉(zhuǎn)為大寫(xiě)字符,進(jìn)入?toUpperCase?的源碼我們發(fā)現(xiàn),這個(gè)看起來(lái)會(huì)修改?String?值的方法,實(shí)際上最后是創(chuàng)建了一個(gè)全新的?String?對(duì)象,而最初的?String?對(duì)象則絲毫未動(dòng)。

          空串與 Null

          空串?""?很好理解,就是長(zhǎng)度為 0 的字符串。可以調(diào)用以下代碼檢查一個(gè)字符串是否為空:

          if(str.length()?==?0){
          ????//?todo
          }

          或者

          if(str.equals("")){
          ?//?todo
          }

          「空串是一個(gè) Java 對(duì)象」, 有自己的串長(zhǎng)度( 0 ) 和內(nèi)容(空),也就是?value?數(shù)組為空。

          String變量還可以存放一個(gè)特殊的值, 名為?null,這表示「目前沒(méi)有任何對(duì)象與該變量關(guān)聯(lián)」。要檢查一個(gè)字符串是否為?null,可如下判斷:

          if(str?==?null){
          ????//?todo
          }

          有時(shí)要檢查一個(gè)字符串既不是?null也不為空串,這種情況下就需要使用以下條件:

          if(str?!=?null?&&?str.length()?!=?0){
          ????//?todo
          }

          有同學(xué)就會(huì)覺(jué)得,這么簡(jiǎn)單的條件判斷還用你說(shuō)?沒(méi)錯(cuò),這雖然簡(jiǎn)單,但仍然有個(gè)小坑,就是我們「必須首先檢查 str 是否為?null,因?yàn)槿绻谝粋€(gè)?null?值上調(diào)用方法,編譯器會(huì)報(bào)錯(cuò)」。

          字符串拼接

          上面既然說(shuō)到?String?是不可變的,我們來(lái)看段代碼,為什么這里的字符串 a 卻發(fā)生了改變?

          String?a?=?"hello";
          String?b?=?"world";
          a?=?a?+?b;?//?a?=?"helloworld"

          實(shí)際上,在使用?+?進(jìn)行字符串拼接的時(shí)候,JVM 是初始化了一個(gè)?StringBuilder?來(lái)進(jìn)行拼接的。相當(dāng)于編譯后的代碼如下:

          String?a?=?"hello";
          String?b?=?"world";
          StringBuilder?builder?=?new?StringBuilder();
          builder.append(a);
          builder.append(b);
          a?=?builder.toString();

          關(guān)于?StringBuilder?下文會(huì)詳細(xì)講解,大家現(xiàn)在只需要知道?StringBuilder?是可變的字符串類(lèi)型就 OK 了。我們看下?builder.toString()?的源碼:

          顯然,toString方法同樣是生成了一個(gè)新的?String?對(duì)象,而不是在舊字符串的內(nèi)容上做更改,相當(dāng)于把舊字符串的引用指向的新的String對(duì)象。這也就是字符串?a?發(fā)生變化的原因。

          另外,我們還需要了解一個(gè)特性,當(dāng)將一個(gè)字符串與一個(gè)非字符串的值進(jìn)行拼接時(shí),后者被自動(dòng)轉(zhuǎn)換成字符串(「任何一個(gè) Java 對(duì)象都可以轉(zhuǎn)換成字符串」)。例如:

          int?age?=?13;
          String?rating?=?"PG"?+?age;?//?rating?=?"PG13"

          這種特性通常用在輸出語(yǔ)句中。例如:

          int?a?=?12;
          System.out.println("a?=?"?+?a);

          結(jié)合上面這兩特性,我們來(lái)看個(gè)小問(wèn)題,「空串和?null?拼接的結(jié)果是啥」?

          String?str?=?null;
          str?=?str?+?"";
          System.out.println(str);

          答案是?null?大家應(yīng)該都能猜出來(lái),但為什么是?null?呢?上文說(shuō)過(guò),使用?+?進(jìn)行拼接實(shí)際上是會(huì)轉(zhuǎn)換為?StringBuilder?使用?append?方法進(jìn)行拼接,編譯后的代碼如下:

          String?str?=?null;
          str?=?str?+?"";
          StringBuilder?builder?=?new?StringBuilder();
          builder.append(str);
          builder.append("");
          str?=?builder.toString();

          看下?append?的源碼:

          可以看出,當(dāng)傳入的字符串是?null?時(shí),會(huì)調(diào)用?appendNull?方法,而這個(gè)方法會(huì)返回?null。

          檢測(cè)字符串是否相等

          可以使用?equals方法檢測(cè)兩個(gè)字符串是否相等。比如:

          String?str?=?"hello";
          System.out.println("hello".equals(str));?//?true

          equals?其實(shí)是?Object?類(lèi)中的一個(gè)方法,所有的類(lèi)都繼承于?Object?類(lèi)。講解?equals?方法之前,我們先來(lái)回顧一下運(yùn)算符?==?的用法,它存在兩種使用情況:

          • 對(duì)于基本數(shù)據(jù)類(lèi)型來(lái)說(shuō),?==?比較的是值是否相同;
          • 對(duì)于引用數(shù)據(jù)類(lèi)型來(lái)說(shuō),?==?比較的是內(nèi)存地址是否相同。

          舉個(gè)例子:

          String?str1?=?new?String("hello");?
          String?str2?=?new?String("hello");
          System.out.println(str1?==?str2);?//?false

          對(duì) Java 中數(shù)據(jù)存儲(chǔ)區(qū)域仍然不明白的可以先回去看看第一章《萬(wàn)物皆對(duì)象》。對(duì)于上述代碼,str1 和 str2 采用構(gòu)造函數(shù)?new String()?的方式新建了兩個(gè)不同字符串,以?String str1 = new String("hello");?為例,new 出來(lái)的對(duì)象存放在堆內(nèi)存中,用一個(gè)引用 str1 來(lái)指向這個(gè)對(duì)象的地址,而這個(gè)對(duì)象的引用 str1 存放在棧內(nèi)存中。str1 和 str2 是兩個(gè)不同的對(duì)象,地址不同,因此?==?比較的結(jié)果也就為?false

          而實(shí)際上,Object?類(lèi)中的原始?equals?方法內(nèi)部調(diào)用的還是運(yùn)算符?==,「判斷的是兩個(gè)對(duì)象是否具有相同的引用(地址),和?==?的效果是一樣的」

          也就是說(shuō),如果你新建的類(lèi)沒(méi)有覆蓋?equals?方法,那么這個(gè)方法比較的就是對(duì)象的地址。而?String?方法覆蓋了?equals?方法,我們來(lái)看下源碼:

          可以看出,String?重寫(xiě)的?equals?方法比較的是對(duì)象的內(nèi)容,而非地址。

          總結(jié)下?equals()的兩種使用情況:

          • 情況 1:類(lèi)沒(méi)有覆蓋?equals()?方法。則通過(guò)?equals()?比較該類(lèi)的兩個(gè)對(duì)象時(shí),等價(jià)于通過(guò)?==?比較這兩個(gè)對(duì)象(比較的是地址)。
          • 情況 2:類(lèi)覆蓋了?equals()?方法。一般來(lái)說(shuō),我們都覆蓋?equals()?方法來(lái)判斷兩個(gè)對(duì)象的內(nèi)容是否相等,比如?String?類(lèi)就是這樣做的。當(dāng)然,你也可以不這樣做。

          舉個(gè)例子:

          String?a?=?new?String("ab");?//?a?為一個(gè)字符串引用
          String?b?=?new?String("ab");?//?b?為另一個(gè)字符串引用,這倆對(duì)象的內(nèi)容一樣

          if?(a.equals(b))?//?true
          ????System.out.println("aEQb");
          if?(a?==?b)?//?false,不是同一個(gè)對(duì)象,地址不同
          ????System.out.println("a==b");

          字符串常量池

          字符串?String?既然作為?Java?中的一個(gè)類(lèi),那么它和其他的對(duì)象分配一樣,需要耗費(fèi)高昂的時(shí)間與空間代價(jià),作為最基礎(chǔ)最常用的數(shù)據(jù)類(lèi)型,大量頻繁的創(chuàng)建字符串,將會(huì)極大程度的影響程序的性能。為此,JVM 為了提高性能和減少內(nèi)存開(kāi)銷(xiāo),在實(shí)例化字符串常量的時(shí)候進(jìn)行了一些優(yōu)化:

          • 為字符串開(kāi)辟了一個(gè)「字符串常量池 String Pool」,可以理解為緩存區(qū)
          • 創(chuàng)建字符串常量時(shí),首先檢查字符串常量池中是否存在該字符串
          • 「若字符串常量池中存在該字符串,則直接返回該引用實(shí)例,無(wú)需重新實(shí)例化」;若不存在,則實(shí)例化該字符串并放入池中。

          舉個(gè)例子:

          String?str1?=?"hello";
          String?str2?=?"hello";

          System.out.printl("str1?==?str2"?:?str1?==?str2?)?//true?

          對(duì)于上面這段代碼,String str1 = "hello";,?「編譯器首先會(huì)在棧中創(chuàng)建一個(gè)變量名為?str1?的引用,然后在字符串常量池中查找有沒(méi)有值為 "hello" 的引用,如果沒(méi)找到,就在字符串常量池中開(kāi)辟一個(gè)地址存放 "hello" 這個(gè)字符串,然后將引用?str1?指向 "hello"」

          需要注意的是,字符串常量池的位置在 JDK 1.7 有所變化:

          • 「JDK 1.7 之前」,字符串常量池存在于「常量存儲(chǔ)」(Constant storage)中
          • 「JDK 1.7 之后」,字符串常量池存在于「堆內(nèi)存」(Heap)中。

          另外,我們還「可以使用?String的?intern()方法在運(yùn)行過(guò)程中手動(dòng)的將字符串添加到 String Pool 中」。具體過(guò)程是這樣的:

          當(dāng)一個(gè)字符串調(diào)用?intern()?方法時(shí),如果 String Pool 中已經(jīng)存在一個(gè)字符串和該字符串的值相等,那么就會(huì)返回 String Pool 中字符串的引用;否則,就會(huì)在 String Pool 中添加一個(gè)新的字符串,并返回這個(gè)新字符串的引用。

          看下面這個(gè)例子:

          String?str1?=?new?String("hello");?
          String?str3?=?str1.intern();
          String?str4?=?str1.intern();
          System.out.println(str3?==?str4);?//?true

          對(duì)于 str3 來(lái)說(shuō),str1.intern()?會(huì)先在 String Pool 中查看是否已經(jīng)存在一個(gè)字符串和 str1 的值相等,沒(méi)有,于是,在 String Pool 中添加了一個(gè)新的值和 str1 相等的字符串,并返回這個(gè)新字符串的引用。

          而對(duì)于 str4 來(lái)說(shuō),str1.intern()在 String Pool 中找到了一個(gè)字符串和 str1 的值相等,于是直接返回這個(gè)字符串的引用。因此 s3 和 s4 引用的是同一個(gè)字符串,也就是說(shuō)它們的地址相同,所以?str3 == str4?的結(jié)果是?true

          ???「總結(jié):」

          • String str = "i"?的方式,java 虛擬機(jī)會(huì)自動(dòng)將其分配到常量池中;

          • String str = new String(“i”)?則會(huì)被分到堆內(nèi)存中。可通過(guò) intern 方法手動(dòng)加入常量池

          new String("hello") 創(chuàng)建了幾個(gè)字符串對(duì)象

          下面這行代碼到底創(chuàng)建了幾個(gè)字符串對(duì)象??jī)H僅只在堆中創(chuàng)建了一個(gè)?

          String?str1?=?new?String("hello");?

          顯然不是。對(duì)于 str1 來(lái)說(shuō),new String("hello")?分兩步走:

          • 首先,"hello" 屬于字符串字面量,因此編譯時(shí)期會(huì)在 String Pool 中查找有沒(méi)有值為 "hello" 的引用,如果沒(méi)找到,就在字符串常量池中開(kāi)辟地址空間創(chuàng)建一個(gè)字符串對(duì)象,指向這個(gè) "hello" 字符串字面量;
          • 然后,使用?new的方式又會(huì)在堆中創(chuàng)建一個(gè)字符串對(duì)象。

          因此,使用這種方式一共會(huì)創(chuàng)建兩個(gè)字符串對(duì)象(前提是 String Pool 中還沒(méi)有 "hello" 字符串對(duì)象)。

          2. 雙生子:可變的 StringBuffer 和 StringBuilder

          String 字符串拼接問(wèn)題

          有些時(shí)候, 需要由較短的字符串構(gòu)建字符串, 例如, 按鍵或來(lái)自文件中的單詞。采用字符串拼接的方式達(dá)到此目的效率比較低。由于 String 類(lèi)的對(duì)象內(nèi)容不可改變,所以每當(dāng)進(jìn)行字符串拼接時(shí),總是會(huì)在內(nèi)存中創(chuàng)建一個(gè)新的對(duì)象。既耗時(shí), 又浪費(fèi)空間。例如:

          String?s?=?"Hello";
          s?+=?"World";

          這段簡(jiǎn)單的代碼其實(shí)總共產(chǎn)生了三個(gè)字符串,即?"Hello"、"World"?和?"HelloWorld"。"Hello" 和 "World" 作為字符串常量會(huì)在 String Pool 中被創(chuàng)建,而拼接操作?+?會(huì) new 一個(gè)對(duì)象用來(lái)存放 "HelloWorld"。

          使用?StringBuilder/ StringBuffer?類(lèi)就可以避免這個(gè)問(wèn)題的發(fā)生,畢竟 String 的?+?操作底層都是由?StringBuilder?實(shí)現(xiàn)的。StringBuilder?和?StringBuffer?擁有相同的父類(lèi):

          但是,StringBuilder?不是線程安全的,在多線程環(huán)境下使用會(huì)出現(xiàn)數(shù)據(jù)不一致的問(wèn)題,而?StringBuffer?是線程安全的。這是因?yàn)樵?StringBuffer?類(lèi)內(nèi),常用的方法都使用了synchronized?關(guān)鍵字進(jìn)行同步,所以是線程安全的。而?StringBuilder?并沒(méi)有。這也是運(yùn)行速度?StringBuilder?大于?StringBuffer?的原因了。因此,如果在單線程下,優(yōu)先考慮使用?StringBuilder。

          初始化操作

          StringBuilder?和?StringBuffer?這兩個(gè)類(lèi)的 API 是相同的,這里就以?StringBuilder??為例演示其初始化操作。

          StringBuiler/StringBuffer不能像?String?那樣直接用字符串賦值,所以也不能那樣初始化。它「需要通過(guò)構(gòu)造方法來(lái)初始化」。首先, 構(gòu)建一個(gè)空的字符串構(gòu)建器:

          StringBuilder?builder?=?new?StringBuilder();

          當(dāng)每次需要添加一部分內(nèi)容時(shí), 就調(diào)用?append?方法:

          char?ch?=?'a';
          builder.append(ch);

          String?str?=?"ert"
          builder.append(str);

          在需要構(gòu)建字符串?String?時(shí)調(diào)用 ?toString?方法, 就能得到一個(gè)?String對(duì)象:

          String?mystr?=?builder.toString();?//?aert

          3. String、StringBuffer、StringBuilder 比較


          可變性線程安全
          String不可變因?yàn)椴豢勺?,所以是線程安全的
          StringBuffer可變線程安全的,因?yàn)槠鋬?nèi)部大多數(shù)方法都使用?synchronized進(jìn)行同步。其效率較低
          StringBuilder可變不是線程安全的,因?yàn)闆](méi)有使用?synchronized進(jìn)行同步,這也是其效率高于 StringBuffer ?的原因。單線程下,優(yōu)先考慮使用 StringBuilder。

          關(guān)于?synchronized?保證線程安全的問(wèn)題,我們后續(xù)文章再說(shuō)。

          ?? References

          • 《Java 核心技術(shù) - 卷 1 基礎(chǔ)知識(shí) - 第 10 版》
          • 《Thinking In Java(Java 編程思想)- 第 4 版》




          ?? 點(diǎn)擊下方卡片關(guān)注公眾號(hào)「飛天小牛肉」(專(zhuān)注于分享計(jì)算機(jī)基礎(chǔ)、Java 基礎(chǔ)和面試指南的相關(guān)原創(chuàng)技術(shù)好文,幫助讀者快速掌握高頻重點(diǎn)知識(shí),有的放矢),與小牛肉一起成長(zhǎng)、共同進(jìn)步

          ???并向大家強(qiáng)烈推薦我維護(hù)的?Gitee 倉(cāng)庫(kù)?「CS-Wiki」(Gitee 推薦項(xiàng)目,目前已 0.9k star。面向全棧,致力于構(gòu)建完善的知識(shí)體系:數(shù)據(jù)結(jié)構(gòu)、計(jì)算機(jī)網(wǎng)絡(luò)、操作系統(tǒng)、算法、數(shù)據(jù)庫(kù)、設(shè)計(jì)模式、Java 技術(shù)棧、機(jī)器學(xué)習(xí)、深度學(xué)習(xí)、強(qiáng)化學(xué)習(xí)等),相比公眾號(hào),該倉(cāng)庫(kù)擁有更健全的知識(shí)體系,歡迎前來(lái) star,倉(cāng)庫(kù)地址 https://gitee.com/veal98/CS-Wiki。也可直接下方掃碼訪問(wèn)


          原創(chuàng)不易,讀完有收獲不妨點(diǎn)贊|分享|在看支持

          瀏覽 62
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  不用AI搜索日韩无码 | 欧日无码一区二区三区在线 | 亚洲中文字幕在线无码 | 免费看黄色的视频 | 伊人成人在线视频 |