Java9中String的優(yōu)化

String類(lèi)的實(shí)現(xiàn)方式

在Java 9之前,String類(lèi)是由char數(shù)組實(shí)現(xiàn)的,每個(gè)char占用兩個(gè)字節(jié)的內(nèi)存空間。而在Java 9中,String類(lèi)引入了一種稱為"Compact Strings"的新實(shí)現(xiàn)方式,將字符串的表示方式從char數(shù)組改為byte數(shù)組,并使用一種編碼方式將Unicode字符映射到一個(gè)或兩個(gè)字節(jié)的表示方式。這種實(shí)現(xiàn)方式可以大大減少內(nèi)存使用,尤其是對(duì)于包含大量ASCII字符的字符串。

為什么這么說(shuō)呢?JDK9中,引入了一個(gè)coder標(biāo)識(shí),用來(lái)區(qū)分是普通的拉丁字母還是UTF16字符。

static final byte LATIN1 = 0;
static final byte UTF16 = 1;

我們?cè)谌粘J褂弥锌赡芎芏嗲闆r下大量使用英文字母,較少使用一些中文或者其他復(fù)雜的字符,這時(shí)候JDK9中的這種優(yōu)化就能有很大的用處,因?yàn)榘凑赵綣DK8的方案,無(wú)論是什么內(nèi)容,就統(tǒng)一按照char來(lái)存儲(chǔ),這樣對(duì)于那些普通的英文字母根本不需要用兩個(gè)字節(jié)的空間,一個(gè)字節(jié)就夠了,如果涉及到大量的這種純英文字母的字符串,此時(shí)JDK9的存儲(chǔ)上的優(yōu)化就大大體現(xiàn)了出來(lái)。

String類(lèi)中的方法

  1. 構(gòu)造方法
  • String(): 創(chuàng)建一個(gè)空字符串。

  • String(char[] value): 創(chuàng)建一個(gè)包含指定字符序列的字符串。

  • String(byte[] bytes): 使用默認(rèn)字符集解碼指定的字節(jié)數(shù)組,創(chuàng)建一個(gè)新的字符串。

  • String(String original): 創(chuàng)建一個(gè)與指定字符串內(nèi)容相同的新字符串。

  1. 字符串操作方法
  • charAt(int index): 返回指定位置上的字符。

  • concat(String str): 將指定字符串連接到此字符串的末尾。

  • substring(int beginIndex): 返回一個(gè)新的字符串,它是此字符串的子字符串。

  • substring(int beginIndex, int endIndex): 返回一個(gè)新的字符串,它是此字符串的子字符串。

  • replace(char oldChar, char newChar): 返回一個(gè)新字符串,它是通過(guò)用新字符替換此字符串中出現(xiàn)的所有舊字符得到的。

  • replaceAll(String regex, String replacement): 用指定的字符串替換所有匹配給定的正則表達(dá)式的子字符串。

  • trim(): 返回字符串的副本,忽略前導(dǎo)空白和尾部空白。

  • toLowerCase(): 使用默認(rèn)語(yǔ)言環(huán)境的規(guī)則將此字符串轉(zhuǎn)換為小寫(xiě)。

  • toUpperCase(): 使用默認(rèn)語(yǔ)言環(huán)境的規(guī)則將此字符串轉(zhuǎn)換為大寫(xiě)。

  • getBytes(): 使用平臺(tái)默認(rèn)字符集將此字符串編碼為字節(jié)數(shù)組。

  1. 字符串比較方法
  • equals(Object anObject): 將此字符串與指定對(duì)象進(jìn)行比較。

  • equalsIgnoreCase(String anotherString): 將此字符串與指定字符串進(jìn)行比較,忽略大小寫(xiě)差異。

  • compareTo(String anotherString): 按字典順序比較兩個(gè)字符串。

  • compareToIgnoreCase(String str): 按字典順序比較兩個(gè)字符串,忽略大小寫(xiě)差異。

  1. 其他方法
  • length(): 返回此字符串的長(zhǎng)度。

  • isEmpty(): 當(dāng)且僅當(dāng)字符串長(zhǎng)度為 0 時(shí)返回 true。

  • valueOf(int i): 返回 int 參數(shù)的字符串表示形式。

  • join(CharSequence delimiter, CharSequence... elements): 將給定的字符串序列以指定的分隔符拼接起來(lái),并返回結(jié)果字符串。

新增的repeat以及strip方法

  1. repeat()方法

repeat(int count)方法可以將原字符串重復(fù)指定次數(shù),并返回一個(gè)新字符串。例如:

javaCopy code
String str = "hello";
String repeatedStr = str.repeat(3);
System.out.println(repeatedStr); // "hellohellohello"

在這個(gè)例子中,我們通過(guò)調(diào)用repeat()方法將字符串"hello"重復(fù)了三次,并返回了一個(gè)新字符串"hellohellohello"。

  1. strip()方法

strip()方法用于去除字符串兩端的空白字符,包括空格、制表符和換行符等,返回一個(gè)新字符串。例如:

javaCopy code
String str = "  hello  \n";
String strippedStr = str.strip();
System.out.println(strippedStr); // "hello"

在這個(gè)例子中,原字符串為" hello \n",包含兩個(gè)前導(dǎo)空格和一個(gè)換行符,調(diào)用strip()方法后返回的新字符串為"hello",兩端的空格和換行符都被去掉了。

除了strip()方法,Java 9還新增了stripLeading()stripTrailing()方法,分別用于去除字符串的前導(dǎo)空格和尾部空格。這些方法對(duì)于處理輸入數(shù)據(jù)和進(jìn)行字符串比較時(shí)非常有用。

字符串常量池的優(yōu)化

在Java 9之前,字符串常量池是在永久代(PermGen)中實(shí)現(xiàn)的。這意味著在運(yùn)行時(shí),所有的字符串常量都被存儲(chǔ)在一塊固定的內(nèi)存區(qū)域中。這種實(shí)現(xiàn)方式存在一些問(wèn)題,比如常量池容易被填滿,導(dǎo)致OutOfMemoryError異常;并且,永久代是Java虛擬機(jī)中一個(gè)相對(duì)較小的區(qū)域,因此可能會(huì)導(dǎo)致內(nèi)存不足的問(wèn)題。

在Java 8中,永久代被移除,字符串常量池被轉(zhuǎn)移到了堆(Heap)中。這種實(shí)現(xiàn)方式解決了一些問(wèn)題,但仍然存在一些性能和內(nèi)存使用方面的問(wèn)題。在Java 9中,字符串常量池進(jìn)行了優(yōu)化,主要包括以下幾個(gè)方面:

  1. 字符串常量池被移到了元空間(Metaspace)中,這是一個(gè)更大的內(nèi)存區(qū)域,可以動(dòng)態(tài)調(diào)整大小,從而避免了OutOfMemoryError異常。

  2. 在元空間中,字符串常量池使用了一種新的數(shù)據(jù)結(jié)構(gòu),稱為“G1特殊化常量池”。這種數(shù)據(jù)結(jié)構(gòu)在性能和內(nèi)存使用方面都有所優(yōu)化,能夠更快地查找和添加常量。

  3. 對(duì)于使用字符串常量的程序,編譯器現(xiàn)在會(huì)生成更高效的字節(jié)碼,以利用這些優(yōu)化。例如,編譯器可以使用ldc2指令來(lái)加載常量池中的雙字節(jié)字符串,而不是使用兩個(gè)ldc指令。

總之,Java 9中對(duì)字符串常量池的優(yōu)化使得它更加高效和可靠,能夠更好地滿足大規(guī)模應(yīng)用程序的需要。

性能分析

前面說(shuō)了這么多,都是關(guān)于JDK9性能上的一些優(yōu)化介紹,但是具體提升了多少呢?下面使用一些直觀的例子來(lái)感受一下到底提升了多少:

字符串拼接

首先來(lái)看看字符串拼接,一直以來(lái)我們編程中有一個(gè)原則:對(duì)于頻繁拼接字符串的操作,不要直接使用String,而是考慮使用StringBuilder或者StringBuffer,這是因?yàn)镾tring的不可變特性,導(dǎo)致在拼接過(guò)程中會(huì)產(chǎn)生大量的String對(duì)象從而導(dǎo)致內(nèi)存浪費(fèi):

運(yùn)行下面這段代碼:

long startTime = System.nanoTime();
String s = "";
for (int i = 0; i < 100000; i++) {
    s += "a";
}
long endTime = System.nanoTime();
System.out.println("cost time: " + (endTime - startTime) + "ns");

將這段代碼分別放到JDK1.8和JDK1.9版本對(duì)比一下執(zhí)行時(shí)間,這里推薦一個(gè)在線的Java編譯運(yùn)行網(wǎng)站,支持動(dòng)態(tài)選擇JDK版本:https://www.jdoodle.com/online-java-compiler

image.png

為了稍微準(zhǔn)確一點(diǎn),排除一定的偶然性,我JDK1.8和JDK1.9各自都運(yùn)行了三遍:

// JDK1.8
cost time: 8043104665ns
cost time: 8029676317ns
cost time: 8084159060ns
//JDK1.9
cost time: 1596969483ns
cost time: 2130420129ns
cost time: 2150930645ns

這里可以看到,在十萬(wàn)級(jí)別的字符串拼接操作中,JDK1.9的性能提升屬于倍數(shù)級(jí)別的提升了,基本都在四倍左右的提升。

同時(shí)每次執(zhí)行時(shí),頁(yè)面上結(jié)果輸出框的上方也有對(duì)應(yīng)的cpu time和memory數(shù)據(jù):

//JDK1.8
1394148 kilobyte(s)
1394176 kilobyte(s)
1394388 kilobyte(s)
//JDK1.9
660312 kilobyte(s)
660324 kilobyte(s)
660500 kilobyte(s)

可以看到,這內(nèi)存占用上直接下降了一個(gè)量級(jí),由此可見(jiàn)JDK1.9在字符串的拼接這塊,性能上得到了巨大的提升。

字符串替換

long startTime = System.nanoTime();
for (int i =  0; i < 100000; i++) {
    String str = "abcdefg";
    str.replace("c", "x");
}
long endTime = System.nanoTime();
System.out.println("cost time: " + (endTime - startTime) + "ns");

同樣的操作,各自執(zhí)行3次看看情況:

//JDK1.8
cost time: 130509721ns
cost time: 226153962ns
cost time: 152726076ns
//JDK1.9
cost time: 53052617ns
cost time: 52959038ns
cost time: 66009582ns
/////////////memory////////
//JDK1.8
97120 kilobyte(s)
97096 kilobyte(s)
96936 kilobyte(s)
//JDK1.9
48488 kilobyte(s)
49688 kilobyte(s)
49732 kilobyte(s)

這里可以看到在效率上,直接下降了一個(gè)量級(jí),同時(shí)在哪存上也有接近50%的節(jié)約。

至于其它一些常規(guī)的方法,比如:查找(indexOf)、分割(split)這里JDK1.8和JDK1.9并沒(méi)有多少提升,甚至可能出現(xiàn)JDK1.9的執(zhí)行效率上反而更低,這并不是說(shuō)9版本中出現(xiàn)了倒退,而是內(nèi)部實(shí)現(xiàn)的算法不一樣,可能在某些比較特殊的場(chǎng)景下,JDK1.9更有優(yōu)勢(shì),就像一些排序算法一樣,雖然時(shí)間復(fù)雜度可以衡量,但是在數(shù)據(jù)量不大的時(shí)候,可能一些復(fù)雜度高的算法反而效果更好。

總體來(lái)說(shuō),JDK1.9中String得split方法相比于JDK1.8,一般認(rèn)為有如下改進(jìn):

  1. 使用新的UTF-16字符串匹配器,代替了JDK 8中使用的正則表達(dá)式引擎。

  2. 改進(jìn)了分隔符的識(shí)別,當(dāng)分隔符為單個(gè)字符時(shí),使用位運(yùn)算進(jìn)行匹配,避免了使用正則表達(dá)式引擎的開(kāi)銷(xiāo)。

  3. 引入了一種優(yōu)化,當(dāng)字符串不包含分隔符時(shí),不進(jìn)行任何操作直接返回原字符串。這里的好處就是避免一些極端情況下產(chǎn)生大量冗余重復(fù)的字符串對(duì)象

  4. 一些內(nèi)部的實(shí)現(xiàn)細(xì)節(jié)上做了微調(diào)。

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

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

  • 首先String不屬于8種基本數(shù)據(jù)類(lèi)型,String是一種引用數(shù)據(jù)類(lèi)型,其默認(rèn)值是null。String類(lèi)在Jav...
    Yaan9閱讀 701評(píng)論 0 1
  • Java之String 開(kāi)篇 下面這段代碼的輸出: String對(duì)象的內(nèi)部實(shí)現(xiàn) 圖示: 在 Java6 以及之前的...
    Cool_Pomelo閱讀 903評(píng)論 2 13
  • 前提回顧 java.lang.String類(lèi)用于描述字符串,Java程序中所有的字符串字面值都可以使用該類(lèi)的對(duì)象加...
    碼界西柚閱讀 254評(píng)論 0 1
  • String 對(duì)象是我們使用最頻繁的一個(gè)對(duì)象類(lèi)型,但它的性能問(wèn)題卻是最容易被忽略的。String 對(duì)象作為 Jav...
    han741閱讀 922評(píng)論 0 1
  • 所有字符串用“”進(jìn)行定義,也可以利用“+”實(shí)現(xiàn)字符串的連接 字符串不是基本數(shù)據(jù)類(lèi)型。 范例:String 類(lèi)的實(shí)例...
    六藝str閱讀 338評(píng)論 0 0

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