1. 程式人生 > >【JDK原始碼分析】String的儲存區與不可變性

【JDK原始碼分析】String的儲存區與不可變性

// ... literals are interned by the compiler 
// and thus refer to the same object
String s1 = "abcd";
String s2 = "abcd";
s1 == s2; // --> true 

// ... These two have the same value
// but they are not the same object
String s1 = new String("abcd");
String s2 = new String("abcd");
s1 == s2; // --> false 

看上面一段程式碼,我們會發生疑惑:為什麼通過字串常量例項化的String型別物件是一樣的,而通過new所建立String物件卻不一樣呢?且看下面分解。

1. 資料儲存區

String是一個比較特殊的類,除了new之外,還可以用字面常量來定義。為了弄清楚這二者間的區別,首先我們得明白JVM執行時資料儲存區,這裡有一張圖對此有清晰的描述:

非共享資料儲存區

非共享資料儲存區是線上程啟動時被建立的,包括:

  • 程式計數器(program counter register)控制執行緒的執行;
  • 棧(JVM Stack, Native Method Stack)儲存方法呼叫與物件的引用等。

共享資料儲存區

該儲存區被所有執行緒所共享,可分為:

  • 堆(Heap)儲存所有的Java物件,當執行new物件時,會在堆裡自動進行記憶體分配。
  • 方法區(Method Area)儲存常量池(run-time constant pool)、欄位與方法的資料、方法與構造器的程式碼。

2. 兩種例項化

例項化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."<init>": (Ljava/lang/String;)V
      18: astore        4
      20: return
}

ldc #2表示從常量池中取#2的常量入棧,astore_1表示將引用存在本地變數1中。因此,我們可以看出:物件onetwothree均指向常量池中的字面常量"Test";物件four是在堆中new的新物件;如下圖所示:

總結如下:

  • 當用字面常量例項化時,String物件儲存在常量池;
  • 當用new例項化時,String物件儲存在堆中;

操作符==比較的是物件的引用,當其指向的物件不同時,則為false。因此,開篇中的程式碼會出現通過new所建立String物件不一樣。

3. 不可變String

String原始碼

JDK7的String類:

public final class String
    implements java.io.Serializable, Comparable<String>, 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引用所指向的物件,如下圖所示:

4. 反射

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類的方法則返回原物件引用。這節省了儲存空間與額外的開銷。

5. 參考資料

相關推薦

JDK原始碼分析String儲存區不可變性

// ... literals are interned by the compiler // and thus refer to the same object String s1 = "abcd"; String s2 = "abcd"; s1 == s2; // --> true // ..

JDK原始碼分析淺談HashMap的原理

這篇文章給出了這樣的一道面試題: 在 HashMap 中存放的一系列鍵值對,其中鍵為某個我們自定義的型別。放入 HashMap 後,我們在外部把某一個 key 的屬性進行更改,然後我們再用這個 key 從 HashMap 裡取出元素,這時候 HashMap 會返回什麼? 文中已給出示例程式碼與答案, k

JDK原始碼分析01-物件序列化ObjectOutputStream

上一篇文章提到反序列化列舉類仍是單例,而普通類物件不是單例,下面我們先分析一下序列化過程,序列化就是呼叫ObjectOutStream的物件的writeObject方法。我們先看一下序列化涉及的比較重要的一個ObjectStreamClass,JDK中的描述是:類的序列化描述

Tomcat9原始碼分析元件框架概述

1 元件與框架介紹 Server:代表整個Catalina Servlet容器,可以包含一個或多個Service Service:包含Connector和Container的集合,Service用適當的Connector接收使用者的請求,

Tomcat9原始碼分析原始碼下載、編譯除錯

一、環境 以下是我編譯Tomcat所採用的環境 OS X Yosemite Version 10.10.5 Intellij IDEA 2017.1 Java version 1.8.0_65 Apache Maven 3.3.9 Tomcat 9.0.0

go原始碼分析go原始碼之slice原始碼分析

Go 語言切片是對陣列的抽象。 Go 陣列的長度不可改變,與陣列相比切片的長度是不固定的,可以追加元素,在追加時可能使切片的容量增大。 len() 和 cap() 函式     切片是可索引的,並且可以由 len() 方法獲取長度。    

go原始碼分析go原始碼之list原始碼分析

本文針對go 1.11版本,路徑src/container/list/list.go 資料結構 Element結構體 Value 前驅 後繼 // Element is an element of a linked list. type Element st

java原始碼分析Map中的hash演算法分析

全網把Map中的hash()分析的最透徹的文章,別無二家。 2018年05月09日 09:08:08 閱讀數:957 你知道HashMap中hash方法的具體實現嗎?你知道HashTable、ConcurrentHashMap中hash方法

spring原始碼分析IOC容器初始化(二)

前言:在【spring原始碼分析】IOC容器初始化(一)中已經分析了匯入bean階段,本篇接著分析bean解析階段。 1.解析bean程式呼叫鏈 同樣,先給出解析bean的程式呼叫鏈: 根據程式呼叫鏈,整理出在解析bean過程中主要涉及的類和相關方法。 2.解析bean原始碼分

NCNN原始碼分析1.基本資料型別

對於NCNN而言,核心在於網路的前向推理過程(Inference),其主要資料型別為mat,該資料型別以類的形式定義在src/mat.h中,其中包含了mat的建構函式、解構函式、常見的運算過程。 #if

Go 原始碼分析從 sort.go 看排序演算法的工程實踐

go version go1.11 darwin/amd64file: src/sort/sort.go 排序演算法有很多種類,比如快排、堆排、插入排序等。各種排序演算法各有其優劣性,在實際生產過程中用到的排序演算法(或者說 Sort 函式)通常是由幾種排序演算法組

PHP7原始碼分析如何理解PHP虛擬機器(一)

順風車運營研發團隊 李樂 1.從物理機說起 虛擬機器也是計算機,設計思想和物理機有很多相似之處; 1.1馮諾依曼體系結構 馮·諾依曼是當之無愧的數字計算機之父,當前計算機都採用的是馮諾依曼體系結構;設計思想主要包含以下幾個方面: 指令和資料不加區別混合儲存在同一個儲

Mybatis原始碼分析13-記一次PageHelper reasonable引數使用不當造成的死迴圈

問題描述及原因 使用Mybatis PageHelper外掛做了表的分頁查詢,要求查詢符合某一條件的所有記錄做處理,就寫了一個迭代器在while迴圈裡對每條記錄做處理,直到符合條件的記錄都處理完程式返回。程式碼如下 public class ReconPaymentI

E2LSH原始碼分析p穩定分佈LSH演算法初探

上一節,我們分析了LSH演算法的通用框架,主要是建立索引結構和查詢近似最近鄰。這一小節,我們從p穩定分佈LSH(p-Stable LSH)入手,逐漸深入學習LSH的精髓,進而靈活應用到解決大規模資料的

Spring原始碼閱讀BeanDefinition原理載入流程

BeanDefinition介面定義及其相關子類實現 在Spring容器初始化過程中,Spring會將配置檔案中配置的Java類封裝成一個個BeanDefinition。 BeanDefinition儲存了具體代表Bean的類,並通過實現了AttributeAccessor介面定義了

MyBatis原始碼分析TypeHandler解析屬性配置元素詳述及相關列舉使用高階進階

TypeHandler解析接著看一下typeHandlerElement(root.evalNode("typeHandlers"));方法,這句讀取的是<configuration>下的<typeHandlers>節點,程式碼實現為:private

Java8原始碼分析併發包-AtomicInteger

AtomicInteger類是實現了原子操作的Integer,以往對於保證int、double、float等基礎型別的運算原子性,需要採用加鎖的方式。但是為了一個簡單的運算操作採用鎖,在多執行緒競爭嚴重的情況下,會導致效能降低,所以在java1.5推出了Atom

proxyConfig原始碼分析proxyTargetClassexposeProxy

本文轉載自shysheng:spring proxyConfig原始碼分析 我們知道,ProxyConfig是所有產生Spring AOP代理物件的基類,它是一個配置源,主要為其AOP代理物件工廠實現類提供基本的配置屬性。 它一共包含5個屬性,本文主要就是分析這5個屬性在產生代理物件過

OkHttp3原始碼分析(一)Request的execute

簡單使用OkHttp3 閱讀本文需要對OkHttp3的使用有一定了解。 首先我們先看看如何簡單進行一個get請求的Request。 Request qqRequest = new Request.Builder()

Java8原始碼分析NIO包-FileChannel

1 概述 Java NIO 由以下幾個核心部分組成: Buffer Channel Selectors 相關類的使用方法可以參考Java NIO 系列教程,寫的通俗易懂。 本文主要從原始碼方面分析一下Channel類。