學(xué)習(xí)資料:
我電腦環(huán)境JDk 1.8
看到一篇很有深度的講解:How many Objects created with: String str=new String("Hello")预皇?
1. String字符串
String
不是Java
中的基本數(shù)據(jù)類型
在C
語言中俊柔,字符串的處理通常是使用char
數(shù)組菜皂,但數(shù)組本身無法封裝字符串操作所需要的方法逮京。在Java
中治唤,String
對(duì)象可以看作是char
數(shù)組的延伸和進(jìn)一步封裝
String
主要由3部分組成:char數(shù)組
花履,offset偏移
牍疏,conut長(zhǎng)度
char數(shù)組
表示String
的內(nèi)容箍铲,String
對(duì)象所表示字符串的超集菩收。String
的真實(shí)內(nèi)容還需要由偏移量和長(zhǎng)度在char
數(shù)組中進(jìn)行定位和截取
1.1 3個(gè)基本特點(diǎn)
不變性
針對(duì)常量池的優(yōu)化
類的final定義
1.1.1 不變性
當(dāng)String
對(duì)象一旦生成梨睁,就不能對(duì)再對(duì)它進(jìn)行改變
個(gè)人理解:
String
類中的操作字符串的方法,并不是直接改變當(dāng)前的String
對(duì)象娜饵,而創(chuàng)建了一個(gè)新的String
對(duì)象
String
的這個(gè)特性可以泛化成不變(immutable)模式
坡贺,即一個(gè)對(duì)象被創(chuàng)建后就不再發(fā)生變化。作用在于當(dāng)一個(gè)對(duì)象需要被多線程共享箱舞,并且訪問頻繁時(shí)遍坟,可以省略同步和鎖等待的時(shí)間
1.1.2 針對(duì)常量池的優(yōu)化
當(dāng)兩個(gè)String
對(duì)象擁有相同的值時(shí),只引用常量池中的同一個(gè)拷貝晴股。當(dāng)一個(gè)字符串反復(fù)出現(xiàn)時(shí)愿伴,可以節(jié)省內(nèi)存空間
String str1 = "abc";
String str2 = "abc";
String str3 = new String("abc");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1 == str3.intern()); // true
str1
與str2
引用了相同的地址,str3
重新開辟了一塊內(nèi)存空間
str3
在常量池中的位置和str1
是一樣的电湘,也就是說隔节,雖然str3
單獨(dú)占用了堆空間,但是它所指向的實(shí)體和str1
完全一樣
str3.intern()
返回了String
對(duì)象在常量池中的引用
1.1.3 類的 final 定義
作為final
類的String
對(duì)象在系統(tǒng)中不可能有任何子類寂呛,這是對(duì)系統(tǒng)安全性的保護(hù)
在JDK 1.5
版本之前的環(huán)境中怎诫,使用final
定義,有助于幫助虛擬機(jī)尋找機(jī)會(huì)贷痪,內(nèi)聯(lián)所有的final
方法幻妓,提高系統(tǒng)效率。但在JDK 1.5
以后劫拢,效果并不明顯
注意:
在1.7
之后肉津,String
的substring()
方法已經(jīng)不會(huì)再引起內(nèi)存泄露
可能會(huì)引起內(nèi)存泄露的是String()
中的一個(gè)私有構(gòu)造方法,在1.7
之后已經(jīng)修復(fù)
1.2 字符串的分割和查找
1.2.1 分割
作者用的
JDK
版本不知道多少舱沧,但應(yīng)該不是1.8
妹沙,很可能是1.6
,感覺同樣的代碼測(cè)試狗唉,時(shí)間已經(jīng)比作者測(cè)試時(shí)間少了不止一個(gè)量級(jí)。除了JDK
代碼迭代升級(jí)的優(yōu)化涡真,還得考慮電腦的差距
結(jié)論:一般情況下直接使用split()
方法足夠分俯,在一些要分割的目標(biāo)長(zhǎng)度很長(zhǎng)很長(zhǎng)并且需要分割的次數(shù)很多很多肾筐,split()
耗時(shí)久時(shí),再考慮使用StringTokenzier
缸剪,但感覺一個(gè)字符串要分割10000次已經(jīng)有些喪心病狂
測(cè)試代碼:
測(cè)試次數(shù)吗铐,為了方便觀察,跨度有些大杏节,應(yīng)該有梯度的測(cè)試
public class StringL {
private static final String LINE = "************************************************";
public static void main(String[] args) {
test(1000);
test(10000);
test(100000);
test(1000000);
test(10000000);
}
/**
* 根據(jù)字符串的長(zhǎng)度完成一輪測(cè)試
*/
private static void test(int num) {
String str = getHugeString(num);
System.out.println("字符串長(zhǎng)度為 " + str.length() + " 唬渗,同一個(gè)字符串需要分割 " + num + " 次時(shí):");
// split 方式
splitTest(str);
// StringTokenizer 方式
StringTokenizerTest(str);
// 打印分割線
System.out.println(LINE);
}
/**
* 使用 StringTokenizer 分割字符串
*/
private static void StringTokenizerTest(String str) {
StringTokenizer st = new StringTokenizer(str, ";");
long now = System.currentTimeMillis();
while (st.hasMoreTokens()) {
st.nextToken();
}
System.out.println("StringTokenizer 用時(shí):" + (System.currentTimeMillis() - now));
}
/**
* 原始的 split 方法分割字符串
*/
private static void splitTest(String str) {
long now = System.currentTimeMillis();
str.split(";");
System.out.println("split 用時(shí):" + (System.currentTimeMillis() - now));
}
/**
* 獲取字符串
*/
private static String getHugeString(int num) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < num; i++) {
sb.append(i);
sb.append(";");
}
return sb.toString();
}
}
輸出結(jié)果,時(shí)間是毫秒:
字符串長(zhǎng)度為 3890 奋渔,同一個(gè)字符串需要分割 1000 次時(shí):
split 用時(shí):2
StringTokenizer 用時(shí):1
************************************************
字符串長(zhǎng)度為 48890 镊逝,同一個(gè)字符串需要分割 10000 次時(shí):
split 用時(shí):4
StringTokenizer 用時(shí):4
************************************************
字符串長(zhǎng)度為 588890 ,同一個(gè)字符串需要分割 100000 次時(shí):
split 用時(shí):28
StringTokenizer 用時(shí):21
************************************************
字符串長(zhǎng)度為 6888890 嫉鲸,同一個(gè)字符串需要分割 1000000 次時(shí):
split 用時(shí):503
StringTokenizer 用時(shí):60
************************************************
字符串長(zhǎng)度為 78888890 撑蒜,同一個(gè)字符串需要分割 10000000 次時(shí):
split 用時(shí):3895
StringTokenizer 用時(shí):770
************************************************
多次測(cè)試,當(dāng)對(duì)同一個(gè)字符串要分割次數(shù)達(dá)不到一定期限時(shí)玄渗,有時(shí)StringTokenizer
的效率還不如split()
方法座菠,只有到達(dá)一定期限后,StringTokenizer
的優(yōu)勢(shì)就很明顯
1.2.1 查找
String
的charAt()藤树,indexOf()
方法非常非常高效
測(cè)試代碼:
分別使用startWith()
和endWith()
比較100w
次
public class CharAtTest {
public static void main(String[] args) {
String str = "abc123456xyz";
int num = 1000000;
String startTag = "abc";
String endTag = "xyz";
// startWith 方法
long now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
boolean b = str.startsWith(startTag);
}
System.out.println("startWith()方法耗時(shí) : " + (System.currentTimeMillis() - now));
// myStartWith() 方法
now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
myStartWith(str);
}
System.out.println("myStartWith()方法耗時(shí) : " + (System.currentTimeMillis() - now));
// endWith方法
now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
boolean b = str.endsWith(endTag);
}
System.out.println("endsWith()方法耗時(shí) : " + (System.currentTimeMillis() - now));
// myEndWith() 方法
int length = str.length();
now = System.currentTimeMillis();
for (int i = 0; i < num; i++) {
myEndWith(str, length);
}
System.out.println("myEndWith()方法耗時(shí) : " + (System.currentTimeMillis() - now));
}
private static void myEndWith(String str, int length) {
boolean b = str.charAt(length - 3) == 'a' && str.charAt(length - 2) == 'b' && str.charAt(length - 1) == 'c';
}
private static void myStartWith(String str) {
boolean b = str.charAt(0) == 'a' && str.charAt(1) == 'b' && str.charAt(2) == 'c';
}
}
輸出結(jié)果:
startWith()方法耗時(shí) : 15
myStartWith()方法耗時(shí) : 6
endsWith()方法耗時(shí) : 13
myEndWith()方法耗時(shí) : 6
結(jié)論:在極為敏感的系統(tǒng)中浴滴,必要的時(shí)候可以考慮使用charAt()
方法來代替startWith()
或者endWith()
。但幾乎大部分情況下岁钓,直接startWith()
或者endWith()
足夠
2. StringBuilder 和 StringBuffer
由于String
對(duì)象是不可變對(duì)象升略,在需要對(duì)字符串進(jìn)行修改操作時(shí),如連接甜紫、替換降宅,總是會(huì)生成新的對(duì)象,導(dǎo)致在某些時(shí)候性能比較差囚霸,JDK
專門提供了創(chuàng)建和修改字符串的工具類StringBuffer
和StringBuilder
2.1 String 常量的累加操作
關(guān)于String s = "a" + "b" + "c"
幾個(gè)對(duì)象: 一個(gè)
String s = "a" + "b" + "c"
這種靜態(tài)字符串的連接操作腰根,Java
在編譯時(shí)期將多個(gè)連接操作的字符串在編譯時(shí)合成一個(gè)單獨(dú)的長(zhǎng)字符串abc
對(duì)于常量字符串的累加,Java
在編譯時(shí)期就做了充分優(yōu)化拓型,在編譯時(shí)期便能確定取值的字符串操作额嘿,在編譯時(shí)期進(jìn)行計(jì)算,在運(yùn)行時(shí)劣挫,并不會(huì)生成大量的String
實(shí)例對(duì)象
2.2 String 變量的累加操作
測(cè)試代碼:
public class StringCreate {
public static void main(String[] args) {
commonConcat();
stringBuilderConcat();
}
private static void stringBuilderConcat() {
long now = System.currentTimeMillis();
for (int i = 0 ; i < 50000; i ++){
StringBuilder sb = new StringBuilder();
String str1 = "abc";
String str2 = "897";
String str3 = "xyz";
String str4 = "123";
String result = sb.append(str1).append(str2).append(str3).append(str4).toString();
}
System.out.println("使用 StringBuilder 用時(shí):" + (System.currentTimeMillis() - now));
}
/**
* 直接拼接
*/
private static void commonConcat() {
long now = System.currentTimeMillis();
for (int i = 0; i < 50000; i++) {
String str1 = "abc";
String str2 = "897";
String str3 = "xyz";
String str4 = "123";
String result = str1 + str2 + str3 + str4;
}
System.out.println("直接拼接用時(shí):" + (System.currentTimeMillis() - now));
}
}
結(jié)果
直接拼接用時(shí):27
使用 StringBuilder 用時(shí):15
平均耗時(shí)差距并不大
原因在于册养,對(duì)于字符串變量的累加,Java
也做了優(yōu)化压固,使用了StringBuidler
對(duì)象來實(shí)現(xiàn)字符串的累加
直接拼接
的代碼中球拦,for()
內(nèi)字符串部分反編譯之后:
反編譯,我并沒有做,而是直接將反編譯的代碼照著書上的形式坎炼,抄了下來
String str1 = "abc";
String str2 = "897";
String str3 = "xyz";
String str4 = "123";
String result = (new StringBuilder(String.valueOf(str1))).append(str2).append(str3).append(str4).toString();
在構(gòu)建超大的String
對(duì)象時(shí)愧膀,優(yōu)先考慮:顯示使用StringBuilder
或者 StringBuffer
2.3 StringBuilder 和 StringBuffer
兩者都繼承之AbstractStringBuiler
,擁有幾乎相同的對(duì)外接口
兩者最大的區(qū)別: StringBuffer
對(duì)幾乎所有的方法做了同步
也就是說選擇需要根據(jù)使用的具體場(chǎng)景來確定谣光,考慮線程安全時(shí)檩淋,多線程環(huán)境就選擇StringBuffer
在兩者的構(gòu)造方法中,都有一個(gè)方法提供了設(shè)置容量參數(shù)
萄金,如果預(yù)先能確定容量蟀悦,指定容量大小,也可以再次對(duì)性能進(jìn)行提升
默認(rèn)情況下氧敢,不指定容量時(shí)日戈,是16
字節(jié),之后需要的容量超過實(shí)際char
數(shù)組長(zhǎng)度時(shí)福稳,就會(huì)進(jìn)行擴(kuò)容涎拉,int newCapacity = (value.length << 1) + 2
,擴(kuò)容的細(xì)節(jié)的圆,暫時(shí)不考慮
若不指定容量鼓拧,擴(kuò)容又基本始終是2
倍大小,就會(huì)造成一些空間浪費(fèi)
3. 最后
有錯(cuò)誤越妈,請(qǐng)指出
共勉 : )