String
String是JAVA中最常用的對象沥匈,就是這樣一個最常用最普通的對象神帅,當(dāng)你深入研究時卻發(fā)現(xiàn)我們并不是真的很了解它挠将,那么讓我們一起來學(xué)習(xí)它吧胳岂!
因為String不可變的性質(zhì),因此Java內(nèi)部實現(xiàn)了常量池舔稀。當(dāng)一個String被創(chuàng)建時乳丰,會先去常量池查看有沒有值相同的示例,有的話直接返回内贮。節(jié)省了內(nèi)存产园,加快了字符串的加載速度汞斧。不可變的對象也可以保證在并發(fā)中保持線程安全
特性
- 字符串常量,實際上也是String對象
- 所有不是通過new創(chuàng)建的String都是放在常量池中
- String類型的對象是不可變的
- String實現(xiàn)了CharSequence接口
String對象創(chuàng)建方式
String str1 = "abcd";
String str2 = new String("abcd");
這兩種不同的創(chuàng)建方法是有差別的什燕,第一種方式是在常量池中拿對象粘勒,第二種方式是直接在堆內(nèi)存空間創(chuàng)建一個新的對象。
只要使用new方法屎即,便需要創(chuàng)建新的對象
連接表達(dá)式+(加號)
- 只有使用引號包含文本的方式創(chuàng)建的String對象之間使用“+”連接產(chǎn)生的新對象才會被加入字符串池中仲义。
- 對于所有包含new方式新建對象(包括null)的“+”連接表達(dá)式,它所產(chǎn)生的新對象都不會被加入字符串池中
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
System.out.println(str3 == str4);//false
String str5 = "string";
System.out.println(str3 == str5);//true
1剑勾、 Sting s; //定義了一個變量s埃撵,沒有創(chuàng)建對象;
2虽另、 = // 賦值暂刘,將某個對象的引用(句柄)賦給s ,沒有創(chuàng)建對象捂刺;
3谣拣、 “abc” //創(chuàng)建一個對象;
4族展、 new String(); // 創(chuàng)建一個對象森缠。
常用方法
length 返回字符串長度
isEmpty 判斷字符串是否為空
charAt 根據(jù)索引位置獲取char
getChars 復(fù)制對應(yīng)位置范圍的char到數(shù)組中
equals, equalsIgnoreCase 對比順序依次為引用地址,char數(shù)組長度仪缸,char數(shù)組內(nèi)容
compareTo 對比字符串大小
startsWith, endsWith 判斷前后綴
hashCode 計算hash值贵涵, 公式為s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
indexOf 查找首次出現(xiàn)的位置
lastIndexOf 查找最后出現(xiàn)的位置
substring 返回子串(舊版本是返回一個引用在父串的一個新串,節(jié)省重新分配內(nèi)存恰画。但實際如果子串引用了一個占用極大的父串宾茂,會因為子串一直被使用導(dǎo)致父串沒法被垃圾回收,新版本substring每次重新復(fù)制char數(shù)組)
concat 拼接字符串(拼接char數(shù)組拴还,重新創(chuàng)建字符串)
replace 用新字符替換所有的舊字符(會先遍歷一次char數(shù)組跨晴,尋找時候存在,再去替換片林,避免每次都要分配char數(shù)組)
matches 判斷是否符合正則 (復(fù)用Pattern.matches()方法)
contains 判斷是否包含子串(復(fù)用indexOf()方法)
replaceFirst 只替換一次
replaceAll 替換所有正則符合的地方
split 按照正則分割字符串
toLowerCase 返回小寫
toUpperCase 返回大寫
trim 去除前后空格
toCharArray 重新復(fù)制char數(shù)組返回
-
join(CharSequence delimiter, CharSequence... elements)
String.join(",", "you", "bao", "luo"); //out: you,bao,luo
equals(Object anObject)
String.equals()代碼邏輯:
判斷傳入的對象與當(dāng)前對象是否為同一個對象端盆,如果是就直接返回true;
判斷傳入的對象是否為String费封,若不是則返回false(如果為null也不成立)焕妙;
判斷傳入的String與當(dāng)前String長度是否一致,若不一致則返回false孝偎;
循環(huán)對比兩個字符串的char[]數(shù)組访敌,逐個對比字符是否一致凉敲,若不一致則直接返回false衣盾;
循環(huán)結(jié)束沒有找到不匹配的則返回true寺旺;
JDK8源碼:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
- intern():naive方法,直接返回常量池中的引用
當(dāng)調(diào)用intern()方法時势决,JVM會在常量池中通過equals()方法查找是否存在等值的String阻塑,如果存在則直接返回常量池中這個String對象的地址;如果不存在則會創(chuàng)建等值的字符串(即等值的char[]數(shù)組字符串果复,但是char[]是新開辟的一份拷貝空間)陈莽,然后再返回這個新創(chuàng)建空間的地址;
在常量池查找等值String時虽抄,通常不止一個字符串而是多個字符串因此效率會比較低走搁,另外為保證唯一性,需要有鎖的介入迈窟;
String str1 = "ab"私植;
String str2 = new String("ab");
System.out.println(str1== str2);//false
System.out.println(str2.intern() == str1);//true
System.out.println(str1== str2);//false
str2 = str2.intern();
System.out.println(str1== str2);//true
知識點
- 在調(diào)用x.toString()的地方可以用""+x替代;
- 字符串的+拼接操作
public static void main(String[] args) throws InterruptedException {
String s = "a";
String st = s + "b" + "c";
}
javap out====>
Code:
stack=3, locals=3, args_size=1
0: ldc #19 // String a
2: astore_1
3: new #21 // class java/lang/StringBuilder
6: dup
7: aload_1
8: invokestatic #23 // Method java/lang/String.valueOf:(Ljava/lang/Object;)Ljava/lang/String;
11: invokespecial #29 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
14: ldc #32 // String b
16: invokevirtual #34 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #38 // String c
21: invokevirtual #34 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #40 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore_2
28: return
- StringBuffer是線程安全操作
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
- StringBuilder非線程安全
public StringBuilder append(String str) { super.append(str); return this; }
System.err.println("hello,world"); ##hello,world實際是String對象
printf格式化輸出
FAQ
String str1 = "abc"; System.out.println(str1 == "abc");
步驟:
a> 棧中開辟一塊空間存放引用str1车酣;
b> String池中開辟一塊空間曲稼,存放String常量"abc";
c> 引用str1指向池中String常量"abc"湖员;
d> str1所指代的地址即常量"abc"所在地址贫悄,輸出為true;String str2 = new String("abc"); System.out.println(str2 == "abc");
步驟:
a> 棧中開辟一塊空間存放引用str2娘摔;
b> 堆中開辟一塊空間存放一個新建的String對象"abc"窄坦;
c> 引用str2指向堆中的新建的String對象"abc";
d> str2所指代的對象地址為堆中地址凳寺,而常量"abc"地址在池中嫡丙,輸出為false;
注意:對于通過new產(chǎn)生的對象读第,會先去常量池檢查有沒有 “abc”曙博,如果沒有,先在常量池創(chuàng)建一個 “abc” 對象怜瞒,然后在堆中創(chuàng)建一個常量池中此 “abc” 對象的拷貝對象父泳;String s2 = new String(“Hello”); 產(chǎn)生幾個對象?
首先吴汪,在jvm的工作過程中惠窄,會創(chuàng)建一片的內(nèi)存空間專門存入string對象。我們把這片內(nèi)存空間叫做string池漾橙;
String s2 = new String(“Hello”);jvm首先在string池內(nèi)里面看找不找到字符串"Hello",如果找到不做任何事情杆融;否則創(chuàng)建新的string對象,放到string池里面霜运。由于遇到了new脾歇,還會在內(nèi)存Heap上(不是string池里面)創(chuàng)建string對象存儲"Hello"蒋腮,并將內(nèi)存上的(不是string池內(nèi)的)string對象返回給s2。
Re: 如果常量池中原來沒有“Hello”, 則創(chuàng)建兩個對象藕各。如果原來的常量池中存在“Hello”時池摧,就是一個對象;其它
String str1 = "a"激况;
String str2 = "b"作彤;
String str3 = str1 + "b";
//str1 和 str2 是字符串常量乌逐,所以在編譯期就確定了竭讳。
//str3 中有個 str1 是引用,所以不會在編譯期確定浙踢。
//又因為String是 final 類型的代咸,所以在 str1 + "b" 的時候?qū)嶋H上是創(chuàng)建了一個新的對象,在把新對象的引用傳給str3
final String str1 = "a"成黄;
String str2 = "b"呐芥;
String str3 = str1 + "b";
//這里和\(3\)的不同就是給 str1 加上了一個final奋岁,這樣str1就變成了一個常量思瘟。
//這樣 str3 就可以在編譯期中就確定了
編譯期優(yōu)化
編譯器在編譯期會針對字符串常量疊加得到固定值,字符串常量包括"hello"或用fianl修飾的變量闻伶,編譯器認(rèn)為這些常量是不可變的
編譯器優(yōu)化String常量連接
示例一
String str = "hello" + "java" + 1;
// 編譯期編譯器會直接編譯為"hellojava1"
#2 = String #21 // hellojava1
#21 = Utf8 hellojava1
示例二
public static final String A = "ab"; // 常量A
public static final String B = "cd"; // 常量B
public static void main(String[] args) {
String s = A + B; // 將兩個常量用+連接對s進(jìn)行初始化
String t = "abcd";
if (s == t) {
System.out.println("s等于t滨攻,它們是同一個對象");
} else {
System.out.println("s不等于t,它們不是同一個對象");
}
}
output ==> s等于t蓝翰,它們是同一個對象
說明:A和B都是常量光绕,值是固定的,因此s的值也是固定的畜份,它在類被編譯時就已經(jīng)確定了诞帐。也就是說:String s=A+B; 等同于:String s="ab"+"cd";
示例三
public static final String A; // 常量A
public static final String B; // 常量B
static {
A = "ab";
B = "cd";
}
public static void main(String[] args) {
// 將兩個常量用+連接對s進(jìn)行初始化
String s = A + B;
String t = "abcd";
if (s == t) {
System.out.println("s等于t,它們是同一個對象");
} else {
System.out.println("s不等于t爆雹,它們不是同一個對象");
}
}
output ==> s不等于t停蕉,它們不是同一個對象
A和B雖然被定義為常量,但是它們都沒有馬上被賦值钙态。在運(yùn)算出s的值之前慧起,他們何時被賦值,以及被賦予什么樣的值册倒,都是個變數(shù)蚓挤。因此A和B在被賦值之前,性質(zhì)類似于一個變量。那么s就不能在編譯期被確定灿意,而只能在運(yùn)行時被創(chuàng)建了
循環(huán)內(nèi)String加操作
- 性能較低的代碼:
public void implicitUseStringBuilder(String[] values) {
String result = "";
for (int i = 0 ; i < values.length; i ++) {
result += values[i];
}
System.out.println(result);
}
編譯后的字節(jié)碼:
public void implicitUseStringBuilder(java.lang.String[]);
Code:
0: ldc #11 // String
2: astore_2
3: iconst_0
4: istore_3
5: iload_3
6: aload_1
7: arraylength
8: if_icmpge 38
11: new #5 // class java/lang/StringBuilder
14: dup
15: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
18: aload_2
19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: aload_1
23: iload_3
24: aaload
25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
31: astore_2
32: iinc 3, 1
35: goto 5
38: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
41: aload_2
42: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
45: return
其中8: if_icmpge 38 和35: goto 5構(gòu)成了一個循環(huán)估灿;
8: if_icmpge 38的意思是如果(i < values.length的相反結(jié)果)成立,則跳到第38行(System.out)脾歧。
35: goto 5則表示直接跳到第5行甲捏。
但是這里面有一個很重要的就是StringBuilder對象創(chuàng)建發(fā)生在循環(huán)之間演熟,也就是意味著有多少次循環(huán)會創(chuàng)建多少個StringBuilder對象鞭执,這樣明顯性能較低
- 性能較高的代碼
public void explicitUseStringBuider(String[] values) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < values.length; i ++) {
result.append(values[i]);
}
}
public void explicitUseStringBuider(java.lang.String[]);
Code:
0: new #5 // class java/lang/StringBuilder
3: dup
4: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
7: astore_2
8: iconst_0
9: istore_3
10: iload_3
11: aload_1
12: arraylength
13: if_icmpge 30
16: aload_2
17: aload_1
18: iload_3
19: aaload
20: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: pop
24: iinc 3, 1
27: goto 10
30: return
從上面可以看出,13: if_icmpge 30
和27: goto 10
構(gòu)成了一個loop循環(huán)芒粹,而0: new #5
位于循環(huán)之外兄纺,所以不會多次創(chuàng)建StringBuilder.
注意:循環(huán)體中需要盡量避免隱式或者顯式創(chuàng)建StringBuilder
不可變的String
String對象是不可變的。 String類中每一個看起來會修改String值的方法化漆,實際上都是創(chuàng)建了一個全新的String對象估脆,以包含修改后的字符串內(nèi)容
String str1 = "java";
String str2 = "java";
System.out.println\("str1=str2 " + \(str1 == str2\)\);
在代碼中,可以創(chuàng)建同一個String對象的多個別名座云,而它們所指的對象是相同的疙赠,一直待在一個單一的物理位置上
重載“+”
在Java中,唯一被重載的運(yùn)算符就是用于String的“+”與“+=”朦拖。除此之外圃阳,Java不允許程序員重載其他的運(yùn)算符
public class StringTest {
String a = "abc";
String b = "mongo";
String info = a + b + 47;
}
String對象是不可變的,所以在上述的代碼過程中可能會是這樣工作的:
- "abc" + "mongo"創(chuàng)建新的String對象abcmongo璧帝;
- "abcmongo" + "47"創(chuàng)建新的String對象abcmongo47捍岳;
- 引用info 指向最終生成的String;
但是這種方式會生成一大堆需要垃圾回收的中間對象睬隶,性能相當(dāng)糟糕
編譯器的優(yōu)化處理
Compiled from "StringTest.java"
public class StringTest {
java.lang.String a;
java.lang.String b;
java.lang.String info;
public StringTest();
Code:
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":
()V
4: aload_0
5: ldc #14 // String abc
7: putfield #16 // Field a:Ljava/lang/String;
10: aload_0
11: ldc #18 // String mongo
13: putfield #20 // Field b:Ljava/lang/String;
16: aload_0
17: new #22 // class java/lang/StringBuilder
20: dup
21: aload_0
22: getfield #16 // Field a:Ljava/lang/String;
25: invokestatic #24 // Method java/lang/String.valueOf:(
Ljava/lang/Object;)Ljava/lang/String;
28: invokespecial #30 // Method java/lang/StringBuilder."<
init>":(Ljava/lang/String;)V
31: aload_0
32: getfield #20 // Field b:Ljava/lang/String;
35: invokevirtual #33 // Method java/lang/StringBuilder.ap
pend:(Ljava/lang/String;)Ljava/lang/StringBuilder;
38: bipush 47
40: invokevirtual #37 // Method java/lang/StringBuilder.ap
pend:(I)Ljava/lang/StringBuilder;
43: invokevirtual #40 // Method java/lang/StringBuilder.to
String:()Ljava/lang/String;
46: putfield #44 // Field info:Ljava/lang/String;
49: return
}
反編譯以上代碼會發(fā)現(xiàn)锣夹,編譯器自動引入了StringBuilder類。
編譯器創(chuàng)建了一個StringBuilder對象苏潜,并調(diào)用StringBuilder.append()方法银萍,最后調(diào)用toString()生成結(jié)果,從而避免中間對象的性能損耗
字符串常量池的設(shè)計思想
- 字符串的分配恤左,和其他的對象分配一樣砖顷,耗費(fèi)高昂的時間與空間代價,作為最基礎(chǔ)的數(shù)據(jù)類型赃梧,大量頻繁的創(chuàng)建字符串滤蝠,極大程度地影響程序的性能
- JVM為了提高性能和減少內(nèi)存開銷,在實例化字符串常量的時候進(jìn)行了一些優(yōu)化
- 為字符串開辟一個字符串常量池授嘀,類似于緩存區(qū)
- 創(chuàng)建字符串常量時物咳,首先堅持字符串常量池是否存在該字符串
- 存在該字符串,返回引用實例蹄皱,不存在览闰,實例化該字符串并放入池中
- 實現(xiàn)的基礎(chǔ)
- 實現(xiàn)該優(yōu)化的基礎(chǔ)是因為字符串是不可變的芯肤,可以不用擔(dān)心數(shù)據(jù)沖突進(jìn)行共享
- 運(yùn)行時實例創(chuàng)建的全局字符串常量池中有一個表,總是為池中每個唯一的字符串對象維護(hù)一個引用,這就意味著它們一直引用著字符串常量池中的對象压鉴,所以崖咨,在常量池中的這些字符串不會被垃圾收集器回收
- 常量池的好處:常量池是為了避免頻繁的創(chuàng)建和銷毀對象而影響系統(tǒng)性能,其實現(xiàn)了對象的共享油吭。例如字符串常量池击蹲,在編譯階段就把所有的字符串文字放到一個常量池中;
- 節(jié)省內(nèi)存空間:常量池中所有相同的字符串常量被合并婉宰,只占用一個空間歌豺;
- 節(jié)省運(yùn)行時間:比較字符串時,==比equals()快心包。對于兩個引用變量类咧,只用==判斷引用是否相等,也就可以判斷實際值是否相等
字符串常量池存儲位置 {#articleHeader1}
字符串常量池則存在于方法區(qū)
String str1 = “abc”;
String str2 = “abc”;
String str3 = “abc”;
String str4 = new String(“abc”);
String str5 = new String(“abc”);