真香!24W字的Java面試手冊(點擊查看)
作者:維常
blog.csdn.net/o9109003234/article/details/109523691
String不可變嗎?
public class App {
public static void main(String[] args) {
String a = "111";
a = "222";
System.out.println(a);
}
}
有的人會認爲上面這段代碼應該輸出:111
這樣才和上面的不變性吻合。
哈哈哈,但是並不是這樣滴。
222
這不對呀,不是不變嗎?怎麼變了呢?
其實在JVM的運行中,會單獨給一塊地分給String。
上面的:
Stirng a="111";
我們知道字符串的分配和其他對象分配一樣,是需要消耗高昂的時間和空間的,而且字符串我們使用的非常多。JVM爲了提高性能和減少內存的開銷,在實例化字符串的時候進行了一些優化:
使用字符串常量池。每當我們創建字符串常量時,JVM會首先檢查字符串常量池,如果該字符串已經存在常量池中,那麼就直接返回常量池中的實例引用。如果字符串不存在常量池中,就會實例化該字符串並且將其放到常量池中。由於String字符串的不可變性我們可以十分肯定常量池中一定不存在兩個相同的字符串。
這裏先去JVM給常量池裏找,找到了就不用創建對象了,直接把對象的引用地址賦給a。找不到會重新創建一個對象,然後把對象的引用地址賦給a。同理a="222";也是先找,找不到就重新創建一個對象,然後把對象的引用地址賦給a。
搜索公縱號:MarkerHub,關注回覆[ vue ]獲取前後端入門教程!
大家有沒有發現我上面的描述中“引用地址”。比如說 Object obj = new Object();很多人喜歡成obj爲對象,其實obj不是對象,他只是一個變量,然後這個變量裏保存一個Object對象的引用地址罷了。
引用類型聲明的變量是指該變量在內存中實際存儲的是一個引用地址,實體在堆中。
所以網上很多文章老喜歡這麼說
User user = new User()
創建了一個user對象,老喜歡把user稱之爲對象。這裏不接受反駁。
所以上面String a = “111”;表達的是變量a裏保存了“111”這個對象的引用地址。變量是可以變的,不能變的是“111”。
String 爲什麼是不可變的?
簡單的來說,String 類中使用 final 關鍵字字符數組保存字符串。代碼如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
}
從上面的這段源碼中可以看出三點:
String 類是final修飾
String存儲內容使用的是char數組
char數組是final修飾
這裏就得複習一下,final有啥用?
當用final修飾一個類時,表明這個類不能被繼承。也就是說,如果一個類你永遠不會讓他被繼承,就可以用final進行修飾。final類中的成員變量可以根據需要設爲final,但是要注意final類中的所有成員方法都會被隱式地指定爲final方法。
當final修飾的方法表示此方法已經是“最後的、最終的”含義,亦即此方法不能被重寫(可以重載多個final修飾的方法)。此處需要注意的一點是:因爲重寫的前提是子類可以從父類中繼承此方法,如果父類中final修飾的方法同時訪問控制權限爲private,將會導致子類中不能直接繼承到此方法,因此,此時可以在子類中定義相同的方法名和參數,此時不再產生重寫與final的矛盾,而是在子類中重新定義了新的方法。(注:類的private方法會隱式地被指定爲final方法。)
當final修飾一個基本數據類型時,表示該基本數據類型的值一旦在初始化後便不能發生變化。如果final修飾一個引用類型時,則在對其初始化之後便不能再讓其指向其他對象了,但該引用所指向的對象的內容是可以發生變化的。本質上是一回事,因爲引用的值是一個地址,final要求值,即地址的值不發生變化。另外final修飾一個成員變量(屬性),必須要顯示初始化。這裏有兩種初始化方式,(1.在申明的時候給其賦值,否則必須在其類的所有構造方法中都要爲其賦值)
比如:
/**
* Description: final修飾變量
* @author : 田維常
* 歡迎關注:java後端技術全棧
*/
public class FinalDemo {
private final String name;
public FinalDemo(String name) {
this.name = name;
}
public FinalDemo() {
}
}
這是會會報錯
關於final就簡單說到這裏
下面來看一個使用String的 案例
/**
* Description:
*
* @author : 田維常
* @date : 2020/11/3
* 歡迎關注公衆號:java後端技術全棧
*/
public class StringDemo {
public static void main(String[] args) {
String name = "老田";
name.concat("!");
System.out.println(name);
System.out.println(name.concat("!"));
}
}
輸出
順道溜達溜達 String中幾個常用方法源碼
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);
//看到了嗎?返回的居然是新的String對象
return new String(buf, true);
}
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
public String replace(char oldChar, char newChar) {
//如果兩個是一樣的,那就必要替換了,所以返回this
if (oldChar != newChar) {
int len = value.length;
int i = -1;
//把當前的char數組複製給val,然後下面基於val來操作
char[] val = value;
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
//創建一個新的char數組
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//創建一個新的String對象
return new String(buf, true);
}
}
return this;
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//正常返回的都是新new出來的String對象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果是該字符串中包含了空格,調用substring方法,否則就是啥都沒幹原本返回
//就是如果字符串裏有空格,那麼還是新生一個String對象返回
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
無論是concat、replace、substring還是trim方法的操作都不是在原有的字符串上進行的,而是重新生成了一個新的字符串對象。也就是說進行這些操作後,最原始的字符串並沒有被改變。
得出兩個結論:
String對象一旦被創建就是固定不變的了,對String對象的任何改變都不影響到原對象,相關的任何變化性的操作都會生成新的對象。
String對象每次有變化性操作的時候,都會從新new一個String對象(這裏指的是有變化的情況)。
回到前面的例子
//String a = "111";相當於
char data [] ={'1','1','1'};
Stirng a = new String(data);
//a = "222";
char data [] ={'2','2','2'};
a = new String(data);
這會變量a裏保存的是"222"對應String對象的引用。
繼續看下面的代碼
public class App {
public static void main(String[] args) {
String a = "111";
String a1 = "111";
String b = new String("111");
//對象地址是同一個
System.out.println(a==a1);
//對象內容是一樣的
System.out.println(a.equals(a1));
//對象地址不一樣
System.out.println(a==b);
//對象內容是一樣的
System.out.println(a.equals(b));
}
}
輸出
true
true
false
true
第一個輸出true,說明a和a1兩個變量保存的引用地址是同一個。
第二個也輸出true,說明a和a1引用地址中內容是一樣的。
a和a1放在棧上,存放着對象的引用地址。
new的對象是在堆中。
常量其實是要看jdk版本的。
所以String a = "111"; 在JVM申請內存存放"111"對應的對象,並將對象保存起來。當String a1="1111";的時候,會先去JVM的那塊地裏尋找是否存在"111",剛好前面保存過,所以找到,然後直接把對象的引用地址給了a1。所以此時的a和a1都保存着同一個引用地址。
接觸java後都知道可以new一個對象。所以 String b = new String("111");就是創建一個對象然後把對象引用地址賦給變量b。但是這裏有個特殊點,那就是(“111”),這裏會先去JVM裏的那塊地裏找找,找到了直接存放引用地址。找不到創建一個對象然後把引用地址給String的有參構造方法裏。
所以第三個中輸出false,因爲a和b所保存的對象引用是不一樣的。
最後一個輸出true。那是因爲兩個變量所保存的引用地址中的內容都是“111”.
答案:
如果常量池中存在,則只需創建一個對象,否則需要創建兩個對象。
如有文章對你有幫助,
“在看”和轉發是對我最大的支持!
點擊文末“閱讀原文”可直達
本文分享自微信公衆號 - Java專欄(finishbug)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。