1. 導(dǎo)讀
上期分享了本人關(guān)于String四個(gè)問(wèn)題, 本期我們繼續(xù)探討String中的兩個(gè)問(wèn)題:
.1 String既然已經(jīng)實(shí)現(xiàn)了Comparable接口, 為什么還要提供內(nèi)部類----CaseInsensitiveComparator;
.2 使用 "+" 拼接String究竟干了什么? 為什么在循環(huán)中不讓使用"+"拼接String;
2. String為什么要提供內(nèi)部類CaseInsensitiveComparator
先來(lái)看下String實(shí)現(xiàn)了Comparable接口后做了什么:
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
String::compareTo做了三件事:
.1 比較兩個(gè)字符串的長(zhǎng)度, 找出最小值;
.2 比較最小長(zhǎng)度中的字符是否相同, 因底層使用ASCII碼存儲(chǔ), 10進(jìn)制的ASCII是純數(shù)字, 可直接減得出比較結(jié)果(compareTo規(guī)定: 返回-1是小于; 0是等于; 1是大于);
.3 如果最小長(zhǎng)度的字符都相同, 再比較兩個(gè)字符串的長(zhǎng)度是否相同;
字符串是可能含有大小寫(xiě)的, 在String::compareTo中認(rèn)為A和a是不同的, 那么在忽略大小寫(xiě)的場(chǎng)景中就不適用了;既然String提供了基于Comparator的內(nèi)部類, 是不是對(duì)這種場(chǎng)景做了特殊處理呢?我們接下來(lái)看CaseInsensitiveComparator的核心實(shí)現(xiàn):
public int compare(String s1, String s2) {
int n1 = s1.length();
int n2 = s2.length();
int min = Math.min(n1, n2);
for (int i = 0; i < min; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
if (c1 != c2) {
c1 = Character.toUpperCase(c1);
c2 = Character.toUpperCase(c2);
if (c1 != c2) {
c1 = Character.toLowerCase(c1);
c2 = Character.toLowerCase(c2);
if (c1 != c2) {
// No overflow because of numeric promotion
return c1 - c2;
}
}
}
}
return n1 - n2;
}
可以看到compare的邏輯和String:compareTo大同小異, 只是在第二步的時(shí)候做了特殊處理:
.1 先將char字符轉(zhuǎn)換成大寫(xiě)作比較(如果是數(shù)字則不變);
.2 如果大寫(xiě)比較不符, 再轉(zhuǎn)換成小寫(xiě)做比較;
.3 如果小寫(xiě)比較還是不符, 證明該char字符為數(shù)字, 直接比較即可;
上面只是說(shuō)明了這兩者實(shí)現(xiàn)的不同, 還是沒(méi)有說(shuō)明為什么這么實(shí)現(xiàn); 要解答這個(gè)首先需要說(shuō)明下Comparable 和 Comparator的異同:
.1 兩者都是接口, 都是實(shí)現(xiàn)對(duì)象的比較的, 返回值都是{-1, 0, 1};
.2 Comparable需要重寫(xiě)Comparable::compareTo方法, 會(huì)對(duì)比較對(duì)象的代碼形成侵入; Comparator由一個(gè)比較目標(biāo)對(duì)象的策略類來(lái)實(shí)現(xiàn), 同時(shí)比較策略則由編寫(xiě)者指定, 無(wú)需侵入比較對(duì)象的代碼;
故而String實(shí)現(xiàn)Comparable接口提供了一種內(nèi)排序的方式, 而Comparator提供了一種不改變比較對(duì)象代碼, 實(shí)現(xiàn)比較的策略, 如果對(duì)CaseInsensitiveComparator的實(shí)現(xiàn)并不滿意, 也可以自己實(shí)現(xiàn)MySelfComparator;
劃重點(diǎn):
.1 CaseInsensitiveComparator的實(shí)現(xiàn)只是String作者提供了一種不同于String::compareTo的比較策略, 如果說(shuō)Compareable是比較的內(nèi)部實(shí)現(xiàn), 那么Comparator就是比較的外部實(shí)現(xiàn);
.2 Comparator這種方式實(shí)現(xiàn)了策略模式, 將變與不變完美分類; 關(guān)于設(shè)計(jì)模式后面再開(kāi)專題分享;
.3 Comparator接口中還有個(gè)equals方法沒(méi)有實(shí)現(xiàn), 不實(shí)現(xiàn)這個(gè)方法為什么不報(bào)錯(cuò)呢? 因?yàn)樗蓄惖母割惗际荗bject, Object::equals已經(jīng)對(duì)這個(gè)方法做了實(shí)現(xiàn), 也就不報(bào)錯(cuò)了;
.4 如果Compareable::compareTo 或者 Comparator::compare的實(shí)現(xiàn)的比較結(jié)果與equals不符時(shí), 你需要考慮這種情況會(huì)不會(huì)有影響;比如HashMap中先調(diào)用equals再調(diào)用的compareTo, 這時(shí)候如果equals與compareTo的結(jié)果是不一致, 不就引起問(wèn)題了; 雖然實(shí)現(xiàn)了Compareable接口不強(qiáng)制重寫(xiě)equals方法, 但是不一致的情況還是需要考慮下的;
3. String字符串拼接的三種方式比較
對(duì)于字符串拼接, 我們可以使用一下三種方式:
.1 "+", 加號(hào)拼接是我們最熟悉的;
.2 concat方法, 調(diào)用String::concat方法實(shí)現(xiàn)拼接;
.3 StringBuild::append方法實(shí)現(xiàn)拼接;
我們先來(lái)看看三種拼接方式的效率差異:
long startTime = System.currentTimeMillis();
String temp = "123";
for(int i = 0; i < 100000; i++) {
temp = temp + "123";
}
System.out.println(String.format("+ 拼接用時(shí): %d毫秒", System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
temp = "123";
for(int i = 0; i < 100000; i++) {
temp = temp.concat("123");
}
System.out.println(String.format("concat 拼接用時(shí): %d毫秒", System.currentTimeMillis() - startTime));
startTime = System.currentTimeMillis();
StringBuilder str = new StringBuilder("123");
for(int i = 0; i < 100000; i++) {
str.append("123");
}
temp = str.toString();
System.out.println(String.format("StringBuilder 拼接用時(shí): %d毫秒", System.currentTimeMillis() - startTime));
這是實(shí)驗(yàn)代碼, 分別使用"+", concat 和 StringBuild::append 進(jìn)行了10萬(wàn)次的字符串拼接; 拼接的字符串統(tǒng)一使用""的靜態(tài)字符串, 從前次的分享可知這種聲明的字符串會(huì)被緩存在JVM的常量池中, 所以三種方式都是對(duì)同一個(gè)對(duì)象的不斷拼接最終形成新的String對(duì)象;那我們來(lái)看看結(jié)果:
這是按上面代碼順序執(zhí)行的結(jié)果, 可以清晰的看到, 在10萬(wàn)這個(gè)數(shù)量級(jí), 使用"+"進(jìn)行拼接字符串的效率明顯低于其他兩種拼接方式, 為什么使用"+"拼接會(huì)這么慢呢?
.1我們來(lái)看下"+"拼接字符串的底層實(shí)現(xiàn):
編譯器對(duì)這種方式做了優(yōu)化: 上面for循環(huán)中的代碼被優(yōu)化成:
temp = new StringBuilder(temp).append("123").toString();
. 每次拼接都會(huì)new 一個(gè)StringBuild對(duì)象;
. 調(diào)用StringBuild::append進(jìn)行拼接;
. 再調(diào)用StringBuild::toString生成新的String對(duì)象;
知道了"+"拼接的底層原理, 試著來(lái)分析下這種方式慢的原因:
.1.1 每次拼接都會(huì)生成兩個(gè)新的對(duì)象, StringBuild 和 String, 創(chuàng)建一次對(duì)象就要消耗一次操作時(shí)間;
.1.2 創(chuàng)建對(duì)象就需要申請(qǐng)內(nèi)存, 而整個(gè)應(yīng)用的內(nèi)存空間是固定的, 循環(huán)次數(shù)多了以后, 必然導(dǎo)致創(chuàng)建對(duì)象時(shí)內(nèi)存不夠用, 這時(shí)候就會(huì)觸發(fā)GC, 而GC為了清理無(wú)效對(duì)象, 會(huì)停止應(yīng)用(stop the world), 這是一個(gè)及其耗時(shí)的操作;
.1.3 "+"拼接的方式慢在創(chuàng)建對(duì)象和GC;
.2 我們?cè)賮?lái)看下concat的拼接:
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);
}
從String::concat的實(shí)現(xiàn)可知這種拼接方式做了什么:
.1 判空, 拼接的字符串是空的, 返回原字符串;
.2 新生成一個(gè)char[], 長(zhǎng)度是舊字符串和拼接字符串的長(zhǎng)度之和;
.3 將舊字符串拷貝到新數(shù)組中;
.4 將拼接字符串拷貝到新數(shù)組中;
.5 返回一個(gè)機(jī)遇新數(shù)組的String對(duì)象;
從底層實(shí)現(xiàn)可看出這種拼接方式的耗時(shí)操作主要是新建String對(duì)象和兩次數(shù)組拷貝操作; 同時(shí)也要看到String::concat也是每次調(diào)用都會(huì)返回一個(gè)新的String對(duì)象;
.3 最后來(lái)看下最快的StringBuild::append的實(shí)現(xiàn):
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
可看出這種方式String::concat的核心思想基本相同, 但是有三點(diǎn)不同:
.1 StringBuild在生成時(shí)會(huì)維護(hù)一個(gè)長(zhǎng)度可變的char[], 默認(rèn)大小是構(gòu)造函數(shù)傳入字符串的長(zhǎng)度加16; 所以每次每次都會(huì)判斷是否拼接字符串的長(zhǎng)度加上已有字符串的長(zhǎng)度是否超過(guò)數(shù)組的長(zhǎng)度; 超過(guò)數(shù)組就擴(kuò)容(大小是當(dāng)前數(shù)組長(zhǎng)度 << 1 + 2), 然后拷貝現(xiàn)有數(shù)據(jù)至新數(shù)組;
.2 判空邏輯更改: 不會(huì)直接返回而會(huì)拼接"null"字符串;
.3 最后就是返回的不是String而是當(dāng)前的StringBuild對(duì)象, 只有在調(diào)用StringBuild::toString時(shí)才會(huì)返回新的String對(duì)象;
StringBuild::append不僅減少新對(duì)象的產(chǎn)生, 連數(shù)組的拷貝操作也盡量減少了, 他拼接耗時(shí)最少也就不足為奇了;
劃重點(diǎn):
.1 字符串拼接耗時(shí):StringBuild::append < String::concat < "+";
.2 在循環(huán)中不要使用"+"進(jìn)行字符串拼接;
.3 對(duì)于上面的例子因?yàn)樯婕暗搅薐VM的常量池, 所以又做了一次驗(yàn)證, 把StringBuild::append 和 “+”的執(zhí)行順序做了對(duì)調(diào), 下面是執(zhí)行的結(jié)果:
第一點(diǎn)的結(jié)論依然成立;
.4 StringBuild 和 StringBuffer都是繼承了AbstractStringBuilder這個(gè)抽象類, 兩個(gè)唯一的區(qū)別就是StringBuffer是線程安全的(所有方法都用了synchronized做了修飾);
這次分享的內(nèi)容就是這些了, 上面內(nèi)容的不正之處歡迎指正; 如果對(duì)于String有其他的問(wèn)題也歡迎一起交流; 最后, 感謝閱讀;