【JDK原始碼分析】String的儲存區與不可變性
導讀 |
我們有時會發生疑惑:為什麼透過字串常量例項化的String型別物件是一樣的,而透過new所建立String物件卻不一樣呢?且看下面分解。 |
String是一個比較特殊的類,除了new之外,還可以用字面常量來定義。為了弄清楚這二者間的區別,首先我們得明白JVM執行時資料儲存區,這裡有一張圖對此有清晰的描述:
非共享資料儲存區是線上程啟動時被建立的,包括:
- 程式計數器(program counter register)控制執行緒的執行;
- 棧(JVM Stack, Native Method Stack)儲存方法呼叫與物件的引用等。
- 堆(Heap)儲存所有的Java物件,當執行new物件時,會在堆裡自動進行記憶體分配。
- 方法區(Method Area)儲存常量池(run-time constant pool)、欄位與方法的資料、方法與構造器的程式碼。
該儲存區被所有執行緒所共享,可分為:
例項化String物件:
public class StringLiterals { public static void main(String[] args) { String one = "Test"; String two = "Test"; String three = "T" + "e" + "s" + "t"; String four = new String("Test"); } }
javap -c StringLiterals反編譯生成位元組碼,我們選取感興趣的部分如下:
public static void main(java.lang.String[]); Code: 0: ldc #2 // String Test 2: astore_1 3: ldc #2 // String Test 5: astore_2 6: ldc #2 // String Test 8: astore_3 9: new #3 // class java/lang/String 12: dup 13: ldc #2 // String Test 15: invokespecial #4 // Method java/lang/String."": (Ljava/lang/String;)V 18: astore 4 20: return }
ldc #2表示從常量池中取#2的常量入棧,astore_1表示將引用存在本地變數1中。因此,我們可以看出:物件one、two、three均指向常量池中的字面常量"Test";物件four是在堆中new的新物件;如下圖所示:
總結如下:
運算子==比較的是物件的引用,當其指向的物件不同時,則為false。因此,開篇中的程式碼會出現透過new所建立String物件不一樣。
JDK7的String類:
public final class String implements java.io.Serializable, Comparable, CharSequence { /** The value is used for character storage. */ private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 }
String類被宣告為final,不可以被繼承,所有的方法隱式地指定為final,因為無法被覆蓋。欄位char value[]表示String類所對應的字串,被宣告為private final;即初始化後不能被修改。常用的new例項化物件String s1 = new String("abcd");的構造器:
public String(String original) { this.value = original.value; this.hash = original.hash; }
只需將value與hash的欄位值進行傳遞即可。
所謂不可變性(immutability)指類不可以透過常用的API被修改。為了更好地理解不可變性,我們先來看《Thinking in Java》中的一段程式碼:
//: operators/Assignment.java // Assignment with objects is a bit tricky. import static net.mindview.util.Print.*; class Tank { int level; } public class Assignment { public static void main(String[] args) { Tank t1 = new Tank(); Tank t2 = new Tank(); t1.level = 9; t2.level = 47; print("1: t1.level: " + t1.level + ", t2.level: " + t2.level); t1 = t2; print("2: t1.level: " + t1.level + ", t2.level: " + t2.level); t1.level = 27; print("3: t1.level: " + t1.level + ", t2.level: " + t2.level); } } /* Output: 1: t1.level: 9, t2.level: 47 2: t1.level: 47, t2.level: 47 3: t1.level: 27, t2.level: 27 *///:~
上述程式碼中,在賦值操作t1 = t2;之後,t1、t2包含的是相同的引用,指向同一個物件。因此對t1物件的修改,直接影響了t2物件的欄位改變。顯然,Tank類是可變的。
也許,有人會說s = s.concat("ef");不是修改了物件s麼?而事實上,我們去看concat的實現,會發現其返回的是新String物件(return new String(buf, true););改變的只是s1引用所指向的物件,如下圖所示:
String的value欄位是final的,可不可以透過過某種方式修改呢?答案是反射。在stackoverflow上有這樣一段修改value欄位的程式碼:
String s1 = "Hello World"; String s2 = "Hello World"; String s3 = s1.substring(6); System.out.println(s1); // Hello World System.out.println(s2); // Hello World System.out.println(s3); // World Field field = String.class.getDeclaredField("value"); field.setAccessible(true); char[] value = (char[])field.get(s1); value[6] = 'J'; value[7] = 'a'; value[8] = 'v'; value[9] = 'a'; value[10] = '!'; System.out.println(s1); // Hello Java! System.out.println(s2); // Hello Java! System.out.println(s3); // World
在上述程式碼中,為什麼物件s2的值也會被修改,而物件s3的值卻不會呢?根據前面的介紹,s1與s2指向同一個物件;所以當s1被修改後,s2也會對應地被修改。至於s3物件為什麼不會?我們來看看substring()的實現:
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }
當beginIndex不為0時,返回的是new的String物件;當beginIndex為0時,返回的是原物件本身。如果將String s3 = s1.substring(6);改為String s3 = s1.substring(0);,那麼物件s3也會被修改了。
如果仔細看java.lang.String.java,我們會發現:當需要改變字串內容時,String類的方法返回的是新String物件;如果沒有改變,String類的方法則返回原物件引用。這節省了儲存空間與額外的開銷。
原文來自:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2679060/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 通過分析 JDK 原始碼研究 Hash 儲存機制JDK原始碼
- java基礎鞏固-淺析String原始碼及其不可變性Java原始碼
- Java原始碼分析:Guava之不可變集合ImmutableMap的原始碼分析Java原始碼Guava
- 【JDK】分析 String str=““ 與 new String()JDK
- Laravel 儲存 (Storage) 原始碼分析Laravel原始碼
- Java中String類不可變性的好處Java
- Fabric 1.0原始碼分析(2) blockfile(區塊檔案儲存)原始碼BloC
- 程式程式碼,常量,區域性變數,全域性變數在記憶體中的儲存位置變數記憶體
- JDK原始碼閱讀-String類JDK原始碼
- 從JDK原始碼看String(上)JDK原始碼
- 從JDK原始碼看String(下)JDK原始碼
- String原始碼分析原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(1)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(2)JDK原始碼
- 【JDK】JDK原始碼分析-AbstractQueuedSynchronizer(3)JDK原始碼
- 【JDK】JDK原始碼分析-ReentrantLockJDK原始碼ReentrantLock
- RocketMQ中Broker的訊息儲存原始碼分析MQ原始碼
- 從原始碼分析RocketMq訊息的儲存原理原始碼MQ
- 【JDK原始碼分析系列】ArrayBlockingQueue原始碼分析JDK原始碼BloC
- JDK原始碼分析-TreeSetJDK原始碼
- 通過String的不變性案例分析Java變數的可變性Java變數
- Java String原始碼分析Java原始碼
- 原始碼分析–ConcurrentHashMap與HashTable(JDK1.8)原始碼HashMapJDK
- 【RocketMQ原始碼分析】深入訊息儲存(2)MQ原始碼
- 【RocketMQ原始碼分析】深入訊息儲存(3)MQ原始碼
- JDK原始碼解析系列之String 之一JDK原始碼
- 原始碼|jdk原始碼之HashMap分析(一)原始碼JDKHashMap
- 原始碼|jdk原始碼之HashMap分析(二)原始碼JDKHashMap
- JDK 1.6 HashMap 原始碼分析JDKHashMap原始碼
- jdk原始碼分析之TreeMapJDK原始碼
- HashMap原始碼分析(JDK 1.8)HashMap原始碼JDK
- jdk原始碼分析之PriorityQueueJDK原始碼
- jdk原始碼分析之WeakHashMapJDK原始碼HashMap
- jdk原始碼分析之ArrayListJDK原始碼
- jdk原始碼分析之HashMapJDK原始碼HashMap
- jdk原始碼分析之CopyOnWriteArrayListJDK原始碼
- Kafka原始碼分析(三) - Server端 - 訊息儲存Kafka原始碼Server
- Java-- String原始碼分析Java原始碼