溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java之怎么理解String

發(fā)布時間:2021-10-21 14:11:50 來源:億速云 閱讀:127 作者:iii 欄目:編程語言

這篇文章主要介紹“Java之怎么理解String”,在日常操作中,相信很多人在Java之怎么理解String問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java之怎么理解String”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

  1. String s = new String("abc")這段代碼創(chuàng)建了幾個對象呢?s=="abc"這個判斷的結(jié)果是什么?s.substring(0,2).intern()=="ab"這個的結(jié)果是什么呢?

  2. s.charAt(index) 真的能表示出所有對應(yīng)的字符嗎?

  3. "abc"+"gbn"+s直接的字符串拼接是否真的比使用StringBuilder的性能低?

前言

很高興遇見你~

Java中的String對象特性,與c/c++語言是很不同的,重點在于其不可變性。那么為了服務(wù)字符串不可變性的設(shè)計,則衍生出非常多的相關(guān)問題:為什么要保持其不可變?底層如何存儲字符串?如何進(jìn)行字符串操作才擁有更好的性能?等等。此外,字符編碼的相關(guān)知識也是非常重要;畢竟,現(xiàn)在使用emoij是再正常不過的事情了。

文章的內(nèi)容圍繞著不可變 這個重點展開:

  1. 分析String對象的不可變性;

  2. 常量池的存儲原理以及intern方法的原理

  3. 字符串拼接的原理以及優(yōu)化

  4. 代碼單元與代碼點的區(qū)別

  5. 總結(jié)

那么,我們開始吧~

不可變性

理解String的不可變性,我們可以簡單看幾行代碼:

String string = "abcd";
String string1 = string.replace("a","b");
System.out.println(string);
System.out.println(string1);

輸出:
abcd
bbcd

string.replace("a","b")這個方法把"abcd"中的a換成了b。通過輸出可以發(fā)現(xiàn),原字符串string并沒有發(fā)生任何改變,replace方法構(gòu)造了一個新的字符串"bbcd"并賦值給了string1變量。這就是String的不可變性。

再舉個栗子:把"abcd"的最后一個字符d改成a,在c/c++語言中,直接修改最后一個字符即可;而在java中,需要重新創(chuàng)建一個String對象:abca,因為"abcd"本身是不可變的,不能被修改。

String對象值是不可變的,一切操作都不會改變String的值,而是通過構(gòu)造新的字符串來實現(xiàn)字符串操作。

很多時候很難理解,為什么Java要如此設(shè)計,這樣不是會導(dǎo)致性能的下降嗎?回顧一下我們?nèi)粘J褂肧tring的場景,更多的時候并沒有直接去修改一個string,而是使用一次,則被拋棄。但下次,很可能,又再一次使用到相同的String對象。例如日志打印:

Log.d("MainActivity",string);

前面的"MainActivity"我們并不需要去更改他,但是卻會頻繁使用到這個字符串。Java把String設(shè)計為不可變,正是為了保持?jǐn)?shù)據(jù)的一致性,使得相同字面量的String引用同個對象。例如:

String s1 = "hello";
String s2 = "hello";

s1s2引用的是同個String對象。如果String可變,那么就無法實現(xiàn)這個設(shè)計了。因此,我們可以重復(fù)利用我們創(chuàng)建過的String對象,而無需重新創(chuàng)建他。

基于重復(fù)使用String的情況比更改String的場景更多的前提下,Java把String設(shè)計為不可變,保持?jǐn)?shù)據(jù)一致性,使得同個字面量的字符串可以引用同個String對象,重復(fù)利用已存在的String對象。

在《Java編程思想》一書中還提到另一個觀點。我們先看下面的代碼:

public String allCase(String s){
    return string.toUpperCase();
}

allCase方法把傳入的String對象全部變成大寫并返回修改后的字符串。而此時,調(diào)用者的期望是傳入的String對象僅僅作為提供信息的作用,而不希望被修改,那么String不可變的特性則非常符合這一點。

使用String對象作為參數(shù)時,我們希望不要改變String對象本身,而String的不可變性符合了這一點。

存儲原理

由于String對象的不可變特性,在存儲上也與普通的對象不一樣。我們都知道對象創(chuàng)建在 上,而String對象其實也一樣,不一樣的是,同時也存儲在 常量池 中。處于堆區(qū)中的String對象,在GC時有極大可能被回收;而常量池中的String對象則不會輕易被回收,那么則可以重復(fù)利用常量池中的String對象。也就是說, 常量池是String對象得以重復(fù)利用的根本原因

常量池不輕易垃圾回收的特性,使得常量池中的String對象可以一直存在,重復(fù)被利用。

往常量池中創(chuàng)建String對象的方式有兩種: 顯式使用雙引號構(gòu)造字符串對象、使用String對象的intern()方法 。這兩個方法不一定會在常量池中創(chuàng)建對象,如果常量池中已存在相同的對象,則會直接返回該對象的引用,重復(fù)利用String對象。其他創(chuàng)建String對象的方法都是在堆區(qū)中創(chuàng)建String對象。舉個栗子吧。

當(dāng)我們通過new String()的方法或者調(diào)用String對象的實例方法,如string.substring()方法,會在堆區(qū)中創(chuàng)建一個String對象。而當(dāng)我們使用雙引號創(chuàng)建一個字符串對象,如String s = "abc",或調(diào)用String對象的intern()方法時,會在常量池中創(chuàng)建一個對象,如下圖所示:

Java之怎么理解String

image.png

還記得我們文章開頭的問題嗎?

  • String s = new String("abc"),這句代碼創(chuàng)建了幾個對象?"abc"在常量池中構(gòu)造了一個對象,new String()方法在堆區(qū)中又創(chuàng)建了一個對象,所以一共是兩個。

  • s=="abc"的結(jié)果是false。兩個不同的對象,一個位于堆中,一個位于常量池中。

  • s.substring(0,2).intern()=="ab" intern方法在常量池中構(gòu)建了一個值為“ab"的String對象,"ab"語句不會再去構(gòu)建一個新的String對象,而是返回已經(jīng)存在的String對象。所以結(jié)果是true。

只有顯式使用雙引號構(gòu)造字符串對象、使用String對象的intern()方法 這兩種方法會在常量池中創(chuàng)建String對象,其他方法都是在堆區(qū)創(chuàng)建對象。每次在常量池創(chuàng)建String對象前都會檢查是否存在相同的String對象,如果是則會直接返回該對象的引用,而不會重新創(chuàng)建一個對象。

關(guān)于intern方法還有一個問題需要講一下,在不同jdk版本所執(zhí)行的具體邏輯是不同的。在jdk6以前,方法區(qū)是存放在永生代內(nèi)存區(qū)域中,與堆區(qū)是分割開的,那么當(dāng)往常量池中創(chuàng)建對象時,就需要進(jìn)行深拷貝,也就是把一個對象完整地復(fù)制一遍并創(chuàng)建新的對象,如下圖:

Java之怎么理解String

image.png

觀察這個代碼:

String s = new String(new char[]{'a'});
s.intern();
System.out.println(s=="a");

在jdk6以前創(chuàng)建的是兩個不同的對象,輸出為false;而jdk7以后常量池中并不會創(chuàng)建新的對象,引用的是同個對象,所以輸出是true。

jdk6之前使用intern創(chuàng)建對象使用的深拷貝,而在jdk7之后使用的是淺拷貝,得以重復(fù)利用堆區(qū)中的String對象。

通過上面的分析,String真正重復(fù)利用字符串是在使用雙引號直接創(chuàng)建字符串時。使用intern方法雖然可以返回常量池中的字符串引用,但是本身已經(jīng)需要堆區(qū)中的一個String對象。因而我們可以得出結(jié)論:

盡量使用雙引號顯式構(gòu)建字符串;如果一個字符串需要頻繁被重復(fù)利用,可以調(diào)用intern方法將他存放到常量池中。

字符串拼接

字符串操作最多的莫過于字符串拼接了,由于String對象的不可變性,如果每次拼接都需要創(chuàng)建新的字符串對象就太影響性能了。因此,官方推出了兩個類: StringBuffer、StringBuilder 。這兩個類可以在不創(chuàng)建新的String對象的前提下拼裝字符串、修改字符串。如下代碼:

StringBuilder stringBuilder = new StringBuilder("abc");
stringBuilder.append("p")
        .append(new char[]{'q'})
        .deleteCharAt(2)
        .insert(2,"abc");
String s = stringBuilder.toString();

拼接、插入、刪除都可以很快速地完成。因此,使用StringBuilder進(jìn)行修改、拼接等操作來初始化字符串是更加高效率的做法。StringBuffer和StringBuilder的接口一致,但StringBuffer對操作方法都加上了synchronize關(guān)鍵字,保證線程安全的同時,也付出了對應(yīng)的性能代價。單線程環(huán)境下更加建議使用StringBuilder。

拼接、修改等操作來初始化字符串時使用StringBuilder和StringBuffer可以提高性能;單線程環(huán)境下使用StringBuilder更加合適。

一般情況下,我們會使用+來連接字符串。+在java經(jīng)過了運算符重載,可以用來拼接字符串。編譯器也對+進(jìn)行了一系列的優(yōu)化。觀察下面的代碼:

String s1 = "ab"+"cd"+"fg";
String s2 = "hello"+s1;

Object object = new Object();
String s3 = s2 + object;
  • 對于s1字符串而言,編譯器會把"ab"+"cd"+"fg"直接優(yōu)化成"abcdefg" ,與String s1 = "abcdefg"; 是等價的。這種優(yōu)化也就減少了拼接時產(chǎn)生的消耗。甚至比使用StringBuilder更加高效。

  • s2的拼接編譯器會自動創(chuàng)建一個StringBuilder來構(gòu)建字符串。也就相當(dāng)于以下代碼:

    StringBuilder sb = new StringBuilder();
    sb.append("hello");
    sb.append(s1);
    String s2 = sb.toString();


    那么這是不是意味著我們可以不需要顯式使用StringBuilder了,反正編譯器都會幫助我們優(yōu)化?當(dāng)然不是,觀察下邊的代碼:

    String s = "a";for(int i=0;i<=100;i++){
        s+=i;
    }


    這里有100次循環(huán),則會創(chuàng)建100個StringBuilder對象,這顯然是一個非常錯誤的做法。這時候就需要我們來顯示創(chuàng)建StringBuilder對象了:

    StringBuilder sb = new StringBuilder("a");for(int i=0;i<=100;i++){
        sb.append(i);
    }
    String s = sb.toString();


    只需要構(gòu)建一個StringBuilder對象,性能就極大地提高了。

  • String s3 = s2 + object; 字符串拼接也是支持直接拼接一個普通的對象,這個時候會調(diào)用該對象的toString方法返回一個字符串來進(jìn)行拼接。toString方法是Object類的方法,若子類沒有重寫,則會調(diào)用Object類的toString方法,該方法默認(rèn)輸出類名+引用地址。這看起來沒有什么問題,但是有一個大坑:切記不要在toString方法中直接使用+拼接自身 。如下代碼

    @Overridepublic String toString() {
        return this+"abc";
    }


    這里直接拼接this會調(diào)用this的toString方法,從而造成了無限遞歸。

Java對+拼接字符串進(jìn)行了優(yōu)化:

  • 可以直接拼接普通對象

  • 字面量直接拼接會合成一個字面量

  • 普通拼接會使用StringBuilder來進(jìn)行優(yōu)化

但同時也有注意這些優(yōu)化是有限度的,我們需要在合適的場景選擇合適的拼接方式來提高性能。

編碼問題

在Java中,一般情況下,一個char對象可以存儲一個字符,一個char的大小是16位。但隨著計算機(jī)的發(fā)展,字符集也在不斷地發(fā)展,16位的存儲大小已經(jīng)不夠用了,因此拓展了使用兩個char,也就是32位來存儲一些特殊的字符,如emoij。一個16位稱為一個 代碼單元 ,一個字符稱為 代碼點 ,一個代碼點可能占用一個代碼單元,也可能是兩個。

在一個字符串中,當(dāng)我們調(diào)用String.length() 方法時,返回的是代碼單元的數(shù)目, String.charAt() 返回也是對應(yīng)下標(biāo)的代碼單元。這在正常情況下并沒有什么問題。而如果允許輸入特殊字符時,這就有大問題了。要獲得真正的代碼點數(shù)目,可以調(diào)用 String .codePointCount 方法;要獲得對應(yīng)的代碼點,可調(diào)用 String.codePointAt 方法。以此來兼容拓展的字符集。

一個字符為一個代碼點,一個char稱為一個代碼單元。一個代碼點可能占據(jù)一個或兩個代碼單元。若允許輸入特殊字符,則必須使用代碼點為單位來操作字符串。

到此,關(guān)于“Java之怎么理解String”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI