網(wǎng)站建設(shè)中素材臺(tái)州seo排名優(yōu)化
1.🥗String類簡(jiǎn)介
在我們寫(xiě)代碼的時(shí)候,String總是充斥著前前后后。
但你會(huì)不會(huì)經(jīng)常力不從心,
“這個(gè)*** 字符串怎么** 轉(zhuǎn)換不成功啊”
“*** 這個(gè)字符串到底是常量還是對(duì)象啊”
“這*** 字符串內(nèi)存結(jié)構(gòu)到底* * * 是什么啊”
“為啥我的字符串?dāng)?shù)組莫名其妙顯示空指針異常啊”
“字符串常量和字符串對(duì)象應(yīng)該怎么比較啊”
“這字符串?dāng)?shù)據(jù)不都是good嘛,怎么比一比又是false了***
“ 字符串怎么轉(zhuǎn)化成int啊啊“
”String類為啥是final修飾的捏?“
”String?StringBuffer??StringBuilder???“
A:”什么?!String不是基本數(shù)據(jù)類型??它還有什么是我不知道的?“
B:”樓上的,你猜一猜String能被繼承嘛?“
A:".....****"
哈哈,知識(shí)點(diǎn)看起來(lái)又多又雜,但我們好好縷一縷,它還是很好理解的。
在C語(yǔ)言中我們涉及到過(guò)字符串,但是在C語(yǔ)言中要表示字符串只能用字符數(shù)組或者字符指針,可以使用標(biāo)準(zhǔn)庫(kù)提供的字符串系列函數(shù)完成大部分操作,但是這種將數(shù)據(jù)和操作數(shù)據(jù)方法分離開(kāi)的方法不符合面向?qū)ο蟮乃枷?#xff0c;而字符串的應(yīng)用又非常廣泛,從上面的對(duì)話中就可以發(fā)現(xiàn),哈哈哈。
因此Java語(yǔ)言專門(mén)提供了String類
我們不論是在寫(xiě)代碼的時(shí)候,還是在校招的筆試當(dāng)中,字符串都是常客。
面試也會(huì)經(jīng)常問(wèn)到關(guān)于String的問(wèn)題
面試官:來(lái)來(lái)小伙子,簡(jiǎn)單背一下String類的源碼呢??小伙子:...我* 你****,(直接丟offer而去)
那么我們就帶著對(duì)它的一些問(wèn)題繼續(xù)往下看
2.🍗 String常用方法和性質(zhì)
2.1 🥩 字符串的構(gòu)造
String類提供給我們的構(gòu)造方式很多,最常見(jiàn)的有以下三種
//使用常量串來(lái)構(gòu)造
String s1 = "hello Gremmie";
System.out.println(s1);
//直接new出String對(duì)象
String s2 = new String("hello 葛玉禮");
System.out.println(s2);
//使用字符數(shù)組進(jìn)行構(gòu)造
char[] array = {'h','e','l','l','o',' ','玉','米'};
String s3 = new String(array);
System.out.println(s3);
🍲?[注意]:
- String是引用類型,內(nèi)部其實(shí)并不存儲(chǔ)字符串本身,在String類的實(shí)現(xiàn)源碼中,String類實(shí)例變量是這樣的👇
public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence,Constable, ConstantDesc {@Stableprivate final byte[] value;
//字符串實(shí)際上就存儲(chǔ)在這個(gè)用final修飾的byte數(shù)組中private final byte coder;/** Cache the hash code for the string */private int hash; // Default to 0
/*** @author Gremmie102* @date 2022/4/21 15:49* @purpose : 比較字符串引用*/
public class StringTestDemo1 {public static void main(String[] args) {//s1和s2引用的是不同的對(duì)象,s1和s3引用的是同一個(gè)對(duì)象String s1 = new String("hello");String s2 = new String("world");String s3 = s1;System.out.println(s1.length());//獲取字符串長(zhǎng)度--輸出5System.out.println(s1.isEmpty());//判斷字符串長(zhǎng)度是否為0,即是否為空}
}
- 在Java中用雙引號(hào)""引起來(lái)的也是String類型對(duì)象
//打印"hello"字符串(String對(duì)象)的長(zhǎng)度 System.out.println("hello".length);
2.2 🍙String對(duì)象的比較
字符串的比較是我們常用的操作,比如:字符串排序
- ==比較是否引用同一個(gè)對(duì)象
這里要注意的是,對(duì)于內(nèi)置類型(基礎(chǔ)數(shù)據(jù)),==比較的是變量中的值,對(duì)于引用類型==比較的是引用中的地址
public static void main(String[] args) {int a = 10;int b = 20;int c = 10;//對(duì)于基本類型變量,==比較兩個(gè)變量中存儲(chǔ)的值是否相同System.out.println(a==b);//falseSystem.out.println(a==c);//true//對(duì)于引用型變量,==比較兩個(gè)引用變量引用的是否為同一個(gè)對(duì)象String s1 = new String("hello");String s2 = new String("hello");String s3 = new String("world");String s4 = s1;System.out.println(s1==s2);//falseSystem.out.println(s2==s3);//falseSystem.out.println(s1==s4);//true}
- boolean equals(Object object)
按照字符大小的順序比較
String重寫(xiě)了父類Object中的equals方法,Object中equals默認(rèn)會(huì)按照==來(lái)比較,String重寫(xiě)equals方法之后,語(yǔ)法規(guī)則為:s1.equals(s2)
我們看看源碼
用代碼來(lái)給大家演示解釋一下👇
public boolean equals(Object anObject){//1.先檢測(cè)this和anObject是否為同一個(gè)對(duì)象比較,如果是的化則返回trueif(this == anObject){return true;}//2.檢測(cè)anObject是否為String類型的對(duì)象,如果是的話那就繼續(xù)比較//不是的話那就返回falseif (anObject instanceof String){String anotherString = (String)anObject;int n = value.length;//3.判斷調(diào)用對(duì)象的this和anObject兩個(gè)字符串的長(zhǎng)度是否相同if (n == anotherString.value.length){char v1[] = value;char v2[] = anotherString.value;int i = 0;//4.按照字典順序,從前往后逐個(gè)字符進(jìn)行比較while(n-- != 0){if (v1[i] != v2[i])return false;i++;}return true;}}return false;}
然后我們?cè)賹?xiě)一個(gè)main方法來(lái)實(shí)驗(yàn)一下
public static void main(String[] args) {String s1 = new String("hello");String s2 = new String("hello");String s3 = new String("HELLO");//s1,s2,s3引用的是三個(gè)不同的對(duì)象,因此==的比較結(jié)果都是falseSystem.out.println(s1 == s2);//falseSystem.out.println(s1 == s3);//false//equals比較:String對(duì)象中的逐個(gè)字符//雖然s1和s2引用的不是同一個(gè)對(duì)象,但是兩個(gè)對(duì)象中放置的內(nèi)容,所以輸出true//s1與s3引用的不是同一個(gè)對(duì)象,而且兩個(gè)對(duì)象中內(nèi)容也不同,因此輸出falseSystem.out.println(s1.equals(s2));//trueSystem.out.println(s1.equals(s3));//false}
- int compareTo(String s)方法:按照字典序進(jìn)行比較
與equals不同的是,equals返回的是boolean類型,而compareTo返回的是int類型。
具體比較方法為:
- 先按照字典順序大小比較,如果出現(xiàn)了不等的字符,則直接返回這兩個(gè)字符的大小差值
- 如果前k個(gè)字符相等(k為兩個(gè)字符長(zhǎng)度最小值)返回值為兩個(gè)字符串的長(zhǎng)度差值
源碼如下👇
測(cè)試代碼如下👇
public static void main(String[] args) {String s1 = new String("abc");String s2 = new String("ac");String s3 = new String("abc");String s4 = new String("abcdef");System.out.println(s1.compareTo(s2));//不同,輸出字符差值-1System.out.println(s1.compareTo(s3));//相同輸出0System.out.println(s1.compareTo(s4));//前k個(gè)字符完全相同,輸出長(zhǎng)度差值-3}
- int compareToIgnoreCase(String str)方法:與compareTo方式相同,但是忽略大小寫(xiě)比較
源碼👇
public static void main(String[] args) {String s1 = new String("abc");String s2 = new String("ac");String s3 = new String("Abc");String s4 = new String("abcdef");System.out.println(s1.compareToIgnoreCase(s2));//不同,輸出字符差值-1System.out.println(s1.compareToIgnoreCase(s3));//相同,輸出0System.out.println(s1.compareToIgnoreCase(s4));//前k個(gè)字符完全相同,輸出長(zhǎng)度差-3}
2.3 🍱 字符串查找
這是一個(gè)超級(jí)實(shí)用的功能,String類中提供了一些常用的查找方法:
**方法 ** | ** 功能 ** |
---|---|
char charAt(int index) | 返回index位置上字符,如果index為負(fù)數(shù)或者越界,拋出 IndexOutOfBoundsException異常 |
int indexOf(int ch) | 返回ch第一次出現(xiàn)的位置,沒(méi)有返回-1 |
int indexOf(int ch, int fromIndex) | 從fromIndex位置開(kāi)始找ch第一次出現(xiàn)的位置,沒(méi)有返回-1 |
int indexOf(String str) | 返回str第一次出現(xiàn)的位置,沒(méi)有返回-1 |
int indexOf(String str, int fromIndex) | 從fromIndex位置開(kāi)始找str第一次出現(xiàn)的位置,沒(méi)有返回-1 |
int lastIndexOf(int ch) | 從后往前找,返回ch第一次出現(xiàn)的位置,沒(méi)有返回-1 |
int lastIndexOf(int ch, int fromIndex) | 從fromIndex位置開(kāi)始找,從后往前找ch第一次出現(xiàn)的位置,沒(méi)有返 回-1 |
int lastIndexOf(String str) | 從后往前找,返回str第一次出現(xiàn)的位置,沒(méi)有返回-1 |
int lastIndexOf(String str, int fromIndex) | 從fromIndex位置開(kāi)始找,從后往前找str第一次出現(xiàn)的位置,沒(méi)有返回-1 |
這里大家要注意的是,上面表格中有 int ch 的參數(shù),是char類型傳進(jìn)來(lái)強(qiáng)轉(zhuǎn)換成int值,以此來(lái)方便程序進(jìn)行,并不是說(shuō)這個(gè)字符就是一個(gè)數(shù)哦
public static void main(String[] args) {
String s = "aaabbbcccaaabbbccc";
System.out.println(s.charAt(3)); // 'b'
System.out.println(s.indexOf('c')); // 6
System.out.println(s.indexOf('c', 10)); // 15
System.out.println(s.indexOf("bbb")); // 3
System.out.println(s.indexOf("bbb", 10)); // 12
System.out.println(s.lastIndexOf('c')); // 17
System.out.println(s.lastIndexOf('c', 10)); // 8
System.out.println(s.lastIndexOf("bbb")); // 12
System.out.println(s.lastIndexOf("bbb", 10)); // 3
2.4 🥡 字符串的轉(zhuǎn)化
我們?cè)谧鲱}的時(shí)候,字符串的轉(zhuǎn)化也是非常重要的,主要有以下幾個(gè)
- 數(shù)值和字符串轉(zhuǎn)化
public static void main(String[] args) {// 數(shù)字轉(zhuǎn)字符串String s1 = String.valueOf(1234);String s2 = String.valueOf(12.34);String s3 = String.valueOf(true);String s4 = String.valueOf(new Student("Hanmeimei", 18));System.out.println(s1);System.out.println(s2);System.out.println(s3);System.out.println(s4);System.out.println("=================================");// 字符串轉(zhuǎn)數(shù)字// 注意:Integer、Double等是Java中的包裝類型,這個(gè)后面會(huì)講到int data1 = Integer.parseInt("1234");double data2 = Double.parseDouble("12.34");System.out.println(data1);System.out.println(data2);
}
- 大小寫(xiě)轉(zhuǎn)換
public static void main(String[] args) {String s1 = "hello";String s2 = "HELLO";// 小寫(xiě)轉(zhuǎn)大寫(xiě)System.out.println(s1.toUpperCase());// 大寫(xiě)轉(zhuǎn)小寫(xiě)System.out.println(s2.toLowerCase());
}
- 字符串轉(zhuǎn)數(shù)組
public static void main(String[] args) {
String s = "hello";
// 字符串轉(zhuǎn)數(shù)組
char[] ch = s.toCharArray();
for (int i = 0; i < ch.length; i++) {
System.out.print(ch[i]);
}
System.out.println();
// 數(shù)組轉(zhuǎn)字符串
String s2 = new String(ch);
System.out.println(s2);
}
個(gè)人覺(jué)得這個(gè)方法是非常實(shí)用的!!
- String格式化輸入
public static void main(String[] args) {
String s = String.format("%d-%d-%d", 2019, 9,14);
System.out.println(s);
}
2.5 🥟 字符串替換
那么在做一些算法題目時(shí),我們需要替換字符串中的內(nèi)容。
好家伙,直接上手利用toCharArray然后幾個(gè)循環(huán)
沒(méi)必要!
我們使用一個(gè)指定的新的字符串替換掉已有的字符串?dāng)?shù)據(jù),可用的方法如下:
方法 | 功能 |
---|---|
String replaceAll(String regex, String replacement) | 替換所有的指定內(nèi)容 |
String replaceFirst(String regex, String replacement) | 替換首個(gè)內(nèi)容 |
String str = "helloworld" ;
System.out.println(str.replaceAll("l", "_"));
System.out.println(str.replaceFirst("l", "_"));
//輸出:
//he__owor_d
//he_loworld
🌭?【注意】:由于字符串是不可變的對(duì)象,替換不修改當(dāng)前字符串,而是產(chǎn)生一個(gè)新的字符串
2.6 🫕 字符串的拆分
我們可以利用split將一個(gè)完整的字符串按照指定的分隔符劃分為若干個(gè)子字符串
方法 | 功能 |
---|---|
String[] split(String regex) | 將字符串全部拆分 |
String[] split(String regex,int limit) | 將字符串以指定的格式,拆分為limit組 |
代碼1👇實(shí)現(xiàn)字符串的拆分
String str = "hello world hello Gremmie" ;
String[] result = str.split(" ") ; // 按照空格拆分
for(String s: result){System.out.println(s);
}
代碼2 👇字符串的部分拆分
public static void main(String[] args) {String str = "hello world hello Gremmie" ;String[] result = str.split(" ",2) ;for(String s: result) {System.out.println(s);}}
運(yùn)行結(jié)果👇
拆分是特別常用的操作,一定要牢牢記住。 有一些特殊字符作為分隔符的話可能無(wú)法正確區(qū)分,需要加上轉(zhuǎn)移符號(hào)
代碼3👇拆分IP地址
public static void main(String[] args) {String str = "106.14.57.10" ;String[] result = str.split("\\.") ;for(String s: result) {System.out.println(s);}}
運(yùn)行結(jié)果👇
🍫?【注意事項(xiàng)】:
- 字符"|","","+"*都得加上轉(zhuǎn)義字符,前面加上?""?.
- 而如果是?""?,那么就得寫(xiě)成?"\"?.
- 如果一個(gè)字符串中有多個(gè)分隔符,可以用"|"作為連字符.
代碼4👇多次拆分
public static void main(String[] args) {String str = "name=Gremmie&age=19" ;String[] result = str.split("&") ;for (int i = 0; i < result.length; i++) {String[] temp = result[i].split("=") ;System.out.println(temp[0]+" = "+temp[1]);}}
運(yùn)行結(jié)果👇
2.7 🥣 字符串的截取
現(xiàn)在我們遇到一種情況,你已經(jīng)知道了索引,想要截取一段字符串 這個(gè)時(shí)候,你是不是又想上手循環(huán)了? 慢著!substring可以滿足你!😋
方法 | 功能 |
---|---|
String substring(int beginIndex) | 從指定索引截取到結(jié)尾 |
String substring(int beginIndex, int endIndex) | 截取部分的內(nèi)容 |
代碼👇字符串的截取
public static void main(String[] args) {String str = "helloGremmie" ;System.out.println(str.substring(5));//是從下標(biāo)為5截到最后的System.out.println(str.substring(0, 5));//下標(biāo)索引范圍左閉右開(kāi),[0,5)}
🍟?注意事項(xiàng):
- 索引要從0開(kāi)始
- 要注意前閉后開(kāi)的規(guī)定,substring(0,5)表示包含0號(hào)下標(biāo)的字符,不包含5號(hào)下標(biāo)
2.8 🍰 字符串前后的空白刪除
String中的trim()方法用來(lái)去掉字符串中的左右兩個(gè)空格,保留中間的空格
代碼示例👇觀察trim()方法的使用
public static void main(String[] args) {String str = " hello Gremmie " ;System.out.println("["+str+"]");System.out.println("["+str.trim()+"]");}
運(yùn)行結(jié)果👇
trim 會(huì)去掉字符串開(kāi)頭和結(jié)尾的空白字符(空格, 換行, 制表符等).
2.9🥫字符串常量池
字符串常量池實(shí)現(xiàn)的前提條件就是Java中String對(duì)象是不可變的,這樣可以安全保證多個(gè)變量共享同一個(gè)對(duì)象。 如果Java中的String對(duì)象可變的話,一個(gè)引用操作改變了對(duì)象的值,那么其他的變量也會(huì)受到影響,顯然這樣是不合理的。 在JDK6.0及之前版本,字符串常量池存放在方法區(qū)中在JDK7.0版本以后,字符串常量池被移到了堆中了。至于為什么移到堆內(nèi),大概是由于方法區(qū)的內(nèi)存空間太小了
2.9.1 🥗 創(chuàng)建對(duì)象(一)
我們先來(lái)看看下面這段代碼,創(chuàng)建String對(duì)象的方式是相同的嘛?
public static void main(String[] args) {String s1 = "hello";String s2 = "hello";String s3 = new String("hello");String s4 = new String("hello");System.out.println(s1 == s2); // trueSystem.out.println(s1 == s3); // falseSystem.out.println(s3 == s4); // false
}
上述程序創(chuàng)建方式類似,為什么s1和s2引用的是同一個(gè)對(duì)象,而s3和s4不是呢? 在Java程序中,類似于:1, 2, 3,3.14,“hello”等字面類型的常量經(jīng)常頻繁使用,為了使程序的運(yùn)行速度更快、 更節(jié)省內(nèi)存,Java為8種基本數(shù)據(jù)類型和String類都提供了常量池。
在系統(tǒng)設(shè)計(jì)中,我們嘗嘗會(huì)使用到”池”的概念。Eg:數(shù)據(jù)庫(kù)連接池,socket連接池,線程池,組件隊(duì)列。”池”可以節(jié)省對(duì)象重復(fù)創(chuàng)建和初始化所耗費(fèi)的時(shí)間。對(duì)那些被系統(tǒng)頻繁請(qǐng)求和使用的對(duì)象,使用此機(jī)制可以提高系統(tǒng)運(yùn)行性能。 ”池”是一種”以空間換時(shí)間”的做法,我們?cè)趦?nèi)存中保存一系列整裝待命的對(duì)象,供人隨時(shí)差遣。與系統(tǒng)效率相比,這些對(duì)象所占用的內(nèi)存空間太微不足道了。
🍳class文件常量池
我們寫(xiě)的每一個(gè)Java類被編譯后,就會(huì)形成一份class文件;class文件中除了包含類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息就是常量池(constant pool table),用于存放編譯器生成的各種 字面量 (Literal)和 符號(hào)引用 (Symbolic References),每個(gè)class文件都有一個(gè)class常量池。 其中 字面量 包括:
- 文本字符串
- 2.八種基本類型的值
- 3.被聲明為final的常量等;
符號(hào)引用 包括:
- 類和方法的全限定名
- 字段的名稱和描述符
- 方法的名稱和描述符。
🥯運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池存在于內(nèi)存中,也就是class常量池被加載到內(nèi)存之后的版本,是方法區(qū)的一部分。不同之處是:它的字面量可以動(dòng)態(tài)的添加(String類的intern()),符號(hào)引用可以被解析為直接引用。 JVM在執(zhí)行某個(gè)類的時(shí)候,必須經(jīng)過(guò)加載、連接、初始化,而連接又包括驗(yàn)證、準(zhǔn)備、解析三個(gè)階段。而當(dāng)類加載到內(nèi)存中后,jvm就會(huì)將class常量池中的內(nèi)容存放到運(yùn)行時(shí)常量池中,由此可知,運(yùn)行時(shí)常量池也是每個(gè)類都有一個(gè)。在解析階段,會(huì)把符號(hào)引用替換為直接引用,解析的過(guò)程會(huì)去查詢字符串常量池,也就是我們下面要說(shuō)的StringTable,以保證運(yùn)行時(shí)常量池所引用的字符串與字符串常量池中是一致的。
2.9.2 🐳 字符串常量池(StringTable)
在HotSpot VM里實(shí)現(xiàn)的string pool功能的是一個(gè)StringTable類,它是一個(gè)Hash表,默認(rèn)值大小長(zhǎng)度是1009;這個(gè)StringTable在每個(gè)HotSpot VM的實(shí)例只有一份,被所有的類共享。字符串常量由一個(gè)一個(gè)字符組成,放在了StringTable上。 在JDK6.0中,StringTable的長(zhǎng)度是固定的,長(zhǎng)度就是1009,因此如果放入String Pool中的String非常多,就會(huì)造成hash沖突,導(dǎo)致鏈表過(guò)長(zhǎng),當(dāng)調(diào)用String#intern()時(shí)會(huì)需要到鏈表上一個(gè)一個(gè)找,從而導(dǎo)致性能大幅度下降;在JDK7.0中,StringTable的長(zhǎng)度可以通過(guò)參數(shù)指定。
JDK版本 | 字符串常量池位置 | 大小設(shè)置 |
---|---|---|
Java6 | 方法區(qū) | 固定:1009 |
Java7 | 堆 | 可設(shè)置,無(wú)大小限制,默認(rèn)60013 |
Java8 | 堆 | 可設(shè)置,有限制,最小1009 |
總結(jié)🍔 :
- 單獨(dú)使用””引號(hào)創(chuàng)建的字符串都是常量,編譯期就已經(jīng)確定存儲(chǔ)到Constant Pool中
- 使用new String(“”)創(chuàng)建的對(duì)象會(huì)存儲(chǔ)到heap中,是運(yùn)行期新創(chuàng)建的;
- 使用只包含常量的字符串連接符如"aa" + "aa"創(chuàng)建的也是常量,編譯期就能確定,已經(jīng)確定存儲(chǔ)到String Pool中,String pool中存有“aaaa”;但不會(huì)存有“aa”。
- 使用包含變量的字符串連接符如”aa” + s1創(chuàng)建的對(duì)象是運(yùn)行期才創(chuàng)建的,存儲(chǔ)在heap中;只要s1是變量,不論s1指向池中的字符串對(duì)象還是堆中的字符串對(duì)象,運(yùn)行期s1 + “aa”操作實(shí)際上是編譯器創(chuàng)建了StringBuilder對(duì)象進(jìn)行了append操作后通過(guò)toString()返回了一個(gè)字符串對(duì)象存在heap上。
- String s2 = “aa” + s1; String s3 = “aa” + s1; 這種情況,雖然s2,s3都是指向了使用包含變量的字符串連接符如”aa” + s1創(chuàng)建的存在堆上的對(duì)象,并且都是s1 + “aa”。但是卻指向兩個(gè)不同的對(duì)象,兩行代碼實(shí)際上在堆上new出了兩個(gè)StringBuilder對(duì)象來(lái)進(jìn)行append操作。在Thinking in java一書(shū)中285頁(yè)的例子也可以說(shuō)明。
- 對(duì)于final String s2 = “111”。s2是一個(gè)用final修飾的變量,在編譯期已知,在運(yùn)行s2+”aa”時(shí)直接用常量“111”來(lái)代替s2。所以s2+”aa”等效于“111”+ “aa”。在編譯期就已經(jīng)生成的字符串對(duì)象“111aa”存放在常量池中。
🍔?注意:
- 在JVM中字符串常量池只有一份,是全局共享的
- 剛開(kāi)始字符串常量池是空的,隨著程序不斷運(yùn)行,字符串常量池中元素會(huì)越來(lái)越多
- 當(dāng)類加載時(shí),字節(jié)碼文件中的常量池也被加載到JVM中,稱為運(yùn)行時(shí)常量池,同時(shí)會(huì)將其中的字符串常量保存在字符串常量池中
- 字符創(chuàng)常量池中的內(nèi)容:一部分來(lái)自運(yùn)行時(shí)常量池,一部分來(lái)自程序動(dòng)態(tài)添加
2.9.3 🦐 創(chuàng)建對(duì)象(二)
創(chuàng)建一個(gè)字符串的過(guò)程👇
String str = new String(“abc") 首先定義一個(gè)str的String類型的引用并存放在棧中 先在字符串常量池中找到該常量是否存在 如果存在則創(chuàng)建一個(gè)引用即可,則在字符串常量池中創(chuàng)建一個(gè)內(nèi)容為"abc"的字符串對(duì)象。 執(zhí)行new操作,在堆中創(chuàng)建一個(gè)指定的對(duì)象"abc",這里堆的對(duì)象是字符串常量池“abc”對(duì)象的一個(gè)拷貝對(duì)象 讓str指向堆中“abc”這個(gè)對(duì)象(也就是存儲(chǔ)這個(gè)對(duì)象的在堆中的地址)
??
在字節(jié)碼文件加載的時(shí)候,先要將.Class文件中的常量池加載到內(nèi)存中稱為運(yùn)行時(shí)常量池,此時(shí)也會(huì)將"hello"字符串保存到字符串常量池當(dāng)中
?
說(shuō)明:
- 在字節(jié)碼文件加載的時(shí)候,"hello"和"world"就已經(jīng)創(chuàng)建好了,并保存在字符串常量池當(dāng)中
- 上圖是代表直接用字符串常量進(jìn)行賦值,即:
public static void main(String[] args) {String s1 = "hello";String s2 = "hello";System.out.println(s1 == s2); // true
}
- 當(dāng)使用String s1 = "hello";創(chuàng)建對(duì)象的時(shí)候,先要在字符串常量池當(dāng)中找,如果找到就將該字符串引用賦值給s1
🥨 intern方法
intern 是一個(gè)native方法(Native方法指:底層使用C++實(shí)現(xiàn)的,看不到其實(shí)現(xiàn)的源代碼),該方法的作用是手動(dòng)將創(chuàng)建的String對(duì)象添加到常量池中。
public static void main(String[] args) {char[] ch = new char[]{'a', 'b', 'c'};String s1 = new String(ch); // s1對(duì)象并不在常量池中//s1.intern(); //調(diào)用之后,會(huì)將s1對(duì)象的引用放入到常量池中String s2 = "abc"; // "abc" 在常量池中存在了,s2創(chuàng)建時(shí)直接用常量池中"abc"的引用System.out.println(s1 == s2);
}
// 輸出false
// 用intern之后,就會(huì)輸出true
不同版本的Java會(huì)有不同的Intern來(lái)實(shí)現(xiàn)
🍜 面試題
請(qǐng)解釋String類當(dāng)中兩種對(duì)象實(shí)例化的區(qū)別?JDK1.8中
- String str = "hello"
只會(huì)開(kāi)辟一塊堆內(nèi)存空間,保存在字符串常量池中,然后str共享常量池中的String對(duì)象
2.?String str = new String("hello")
會(huì)開(kāi)辟兩塊堆內(nèi)存空間,字符串"hello"保存在字符串常量池中,然后用常量池中的String對(duì)象給新開(kāi)辟的String對(duì)象賦值。
3.?String str = new String(new char[]{'h', 'e', 'l', 'l', 'o'})
先在堆上創(chuàng)建一個(gè)String對(duì)象,然后利用copyof將重新開(kāi)辟數(shù)組空間,將參數(shù)字符串?dāng)?shù)組中內(nèi)容拷貝到String對(duì)象中
2.10 🦑 字符串的不可變性
String是一種不可變對(duì)象. 字符串中的內(nèi)容是不可改變的。
不可變性的體現(xiàn)在: 當(dāng)對(duì)字符串重新賦值時(shí)我們需要重新制定一個(gè)內(nèi)存區(qū)域,然后才能賦值,不能對(duì)原有的value進(jìn)行賦值(也就是我們不能改變?cè)械膙alue) 這里其實(shí)就是我們每當(dāng)有一個(gè)對(duì)象就有一次給value賦值的機(jī)會(huì),這次機(jī)會(huì)用了之后,也就是賦值之后,就不可以再改變了 所以我們?cè)谶@里說(shuō),字符串的值一旦給定就不可以再修改,一旦嘗試修改,就會(huì)創(chuàng)建一個(gè)新的字符串對(duì)象,讓這個(gè)新的字符串去接收你想要成為的值。
- String類在設(shè)計(jì)時(shí)就是不可改變的,String類實(shí)現(xiàn)描述中已經(jīng)說(shuō)明了
String類中的字符實(shí)際保存在內(nèi)部維護(hù)的value字符數(shù)組中,該圖還可以看出:
- String類被final修飾,表明該類不能被繼承
- value被修飾被final修飾,表明value自身的值不能改變,即不能引用其它字符數(shù)組,但是其引用空間中的內(nèi)容可以修改。
- 所有涉及到可能修改字符串內(nèi)容的操作都是創(chuàng)建一個(gè)新對(duì)象,改變的是新對(duì)象
它是需要新創(chuàng)建一個(gè)對(duì)象再對(duì)其進(jìn)行修改的,并不是在原有的基礎(chǔ)上進(jìn)行修改
有些人說(shuō):字符串不可變是因?yàn)槠鋬?nèi)部保存字符的數(shù)組被final修飾了,因此不能改變。 這種說(shuō)法是錯(cuò)誤的,不是因?yàn)镾tring類自身,或者其內(nèi)部value被final修飾而不能被修改。?final修飾類表明該類不想被繼承,final修飾引用類型表明該引用變量不能引用其他對(duì)象,但是其引用對(duì)象中的內(nèi)容是可以修改的。
public static void main(String[] args) {final int array[] = {1,2,3,4,5};array[0] = 100;System.out.println(Arrays.toString(array));
// array = new int[]{4,5,6}; // 編譯報(bào)錯(cuò):Error:(19, 9) java: 無(wú)法為最終變量array分配值
}
為什么 String 要涉及成不可變的?(不可變對(duì)象的好處是什么?)
- 方便實(shí)現(xiàn)字符串對(duì)象池. 如果 String 可變, 那么對(duì)象池就需要考慮寫(xiě)時(shí)拷貝的問(wèn)題了.
- 不可變對(duì)象是線程安全的.
- 不可變對(duì)象更方便緩存 hash code, 作為 key 時(shí)可以更高效的保存到 HashMap 中.
2.11 🐙 字符串的修改
**注意:盡量避免直接對(duì)String類型對(duì)象進(jìn)行修改,因?yàn)镾tring類是不能修改的,所有的修改都會(huì)創(chuàng)建新對(duì)象,效率 非常低下。 **
比如這么做👇
public static void main(String[] args) {String s = "hello";s += " world";System.out.println(s); // 輸出:hello world
}
這么做中間創(chuàng)建了很多臨時(shí)對(duì)象,非常浪費(fèi)資源
那么我們應(yīng)該怎么做呢?
在對(duì)String類進(jìn)行修改時(shí),效率是非常慢的,因此:盡量避免對(duì)String的直接需要,如果要修改建議盡量 使用StringBuffer或者StringBuilder。
3. 🦞 Java StringBuffer 和 StringBuilder 類
當(dāng)對(duì)字符串進(jìn)行修改的時(shí)候,需要使用 StringBuffer 和 StringBuilder 類。
和 String 類不同的是,StringBuffer 和 StringBuilder 類的對(duì)象能夠被多次的修改,并且不產(chǎn)生新的未使用對(duì)象。
[圖片上傳失敗...(image-b74cd7-1665193486312)]
在使用 StringBuffer 類時(shí),每次都會(huì)對(duì) StringBuffer 對(duì)象本身進(jìn)行操作,而不是生成新的對(duì)象,所以如果需要對(duì)字符串進(jìn)行修改推薦使用 StringBuffer。 StringBuilder 類在 Java 5 中被提出,它和 StringBuffer 之間的最大不同在于 StringBuilder 的方法不是線程安全的(不能同步訪問(wèn))。 由于 StringBuilder 相較于 StringBuffer 有速度優(yōu)勢(shì),所以多數(shù)情況下建議使用 StringBuilder 類。
那么StringBuilder在append的時(shí)候具體是怎么做的呢?
public static void main(String args[]){StringBuilder sb = new StringBuilder(10);sb.append("Runoob..");System.out.println(sb); sb.append("!");System.out.println(sb); sb.insert(8, "Java");System.out.println(sb); sb.delete(5,8);System.out.println(sb); }
運(yùn)行結(jié)果為: Runoob.. Runoob..! Runoob..Java! RunooJava!
而如果我們要求線程安全的話,就必須使用StringBuffer類了
public class Test{public static void main(String args[]){StringBuffer sBuffer = new StringBuffer("Gremmie:");sBuffer.append("http://");sBuffer.append("106.14.57.");sBuffer.append("10/");System.out.println(sBuffer); }
}
運(yùn)行結(jié)果為👇
那么總結(jié)一下StringBuilder常用的一樣方法:
方法 | 說(shuō)明 |
---|---|
StringBuff append(String str) | 在尾部追加,相當(dāng)于String的+=,可以追加:boolean、char、char[]、 double、float、int、long、Object、String、StringBuff的變量 |
char charAt(int index) | 獲取index位置的字符 |
int length() | 獲取字符串的長(zhǎng)度 |
int capacity() | 獲取底層保存字符串空間總的大小 |
void ensureCapacity(int mininmumCapacity) | 擴(kuò)容 |
void setCharAt(int index, char ch) | 將index位置的字符設(shè)置為ch |
int indexOf(String str) | 返回str第一次出現(xiàn)的位置 |
int indexOf(String str, int fromIndex) | 從fromIndex位置開(kāi)始查找str第一次出現(xiàn)的位置 |
int lastIndexOf(String str) | 返回最后一次出現(xiàn)str的位置 |
int lastIndexOf(String str, int fromIndex) | 從fromIndex位置開(kāi)始找str最后一次出現(xiàn)的位置 |
StringBuff insert(int offset, String str) | 在offset位置插入:八種基類類型 & String類型 & Object類型數(shù)據(jù) |
StringBuffer deleteCharAt(int index) | 刪除index位置字符 |
StringBuffer delete(int start, int end) | 刪除[start, end)區(qū)間內(nèi)的字符 |
StringBuffer replace(int start, int end, String str ) | 將[start, end)位置的字符替換為str |
String substring(int start) | 從start開(kāi)始一直到末尾的字符以String的方式返回 |
String substring(int start,int end) | 將[start, end)范圍內(nèi)的字符以String的方式返回 |
StringBuffer reverse() | 反轉(zhuǎn)字符串 |
String toString() | 將所有字符按照String的方式返回 |
public static void main(String[] args) {StringBuilder sb1 = new StringBuilder("hello");StringBuilder sb2 = sb1;// 追加:即尾插-->字符、字符串、整形數(shù)字sb1.append(' '); // hellosb1.append("world"); // hello worldsb1.append(123); // hello world123System.out.println(sb1); // hello world123System.out.println(sb1 == sb2); // trueSystem.out.println(sb1.charAt(0)); // 獲取0號(hào)位上的字符 hSystem.out.println(sb1.length()); // 獲取字符串的有效長(zhǎng)度14System.out.println(sb1.capacity()); // 獲取底層數(shù)組的總大小sb1.setCharAt(0, 'H'); // 設(shè)置任意位置的字符 Hello world123sb1.insert(0, "Hello world!!!"); // Hello world!!!Hello world123System.out.println(sb1);System.out.println(sb1.indexOf("Hello")); // 獲取Hello第一次出現(xiàn)的位置System.out.println(sb1.lastIndexOf("hello")); // 獲取hello最后一次出現(xiàn)的位置sb1.deleteCharAt(0); // 刪除首字符sb1.delete(0,5); // 刪除[0, 5)范圍內(nèi)的字符String str = sb1.substring(0, 5); // 截取[0, 5)區(qū)間中的字符以String的方式返回System.out.println(str);sb1.reverse(); // 字符串逆轉(zhuǎn)str = sb1.toString(); // 將StringBuffer以String的方式返回System.out.println(str);
}
從上述例子可以看出:String和StringBuilder最大的區(qū)別在于String的內(nèi)容無(wú)法修改,而StringBuilder的內(nèi)容可 以修改。頻繁修改字符串的情況考慮使用StringBuilder
注意:?String和StringBuilder類不能直接轉(zhuǎn)換。
如果要想互相轉(zhuǎn)換,可以采用如下原則 :
- String變?yōu)镾tringBuilder: 利用StringBuilder的構(gòu)造方法或append()方法
- StringBuilder變?yōu)镾tring: 調(diào)用toString()方法
🍜 面試題
String、StringBuffer、StringBuilder的區(qū)別
- String的內(nèi)容不可修改,StringBuffer與StringBuilder的內(nèi)容可以修改.
- StringBuffer與StringBuilder大部分功能是相似的
- StringBuffer采用同步處理,屬于線程安全操作;而StringBuilder未采用同步處理,屬于線程不安全操作
- 以下總共創(chuàng)建了多少個(gè)String對(duì)象【前提不考慮常量池之前是否存在】 String str = new String("ab"); // 會(huì)創(chuàng)建多少個(gè)對(duì)象 String str = new String("a") + new String("b"); // 會(huì)創(chuàng)建多少個(gè)對(duì)象