聊一聊Java字元串的不可變

  • 2019 年 10 月 5 日
  • 筆記

前言

在 Java 開發中 String (字元串)對象是我們使用最頻繁的對象,也是很重要的對象。正是使用得如此頻繁,String 在實現層面上不斷進行優化,從 Java6 到 Java7,再到 Java9 的新實現 ,都是為了提升 String 對象的性能,而其中不變的是 String 所生俱來的特性:不可變。本文主要聊一聊 String 的不可變,以及為什麼存在的。

什麼是 String 的不可變

首先我們先來看下什麼是不可變對象:一旦對象被創建並初始化後,內部的狀態數據就會保持不變。查看 JDK 源碼中的 String 類,可以看到類本身被 final 修飾,並且內部的大部分屬性都是 final 修飾的,除了欄位 hash 是通過字元串內容計算並快取起來的。這樣的行為讓 String 類無法被擴展,內部屬性也無法被修改。

接著我們再來用畫圖的形式來說明下 String 的不可變性。

通常我們初始化字元串都是以下形式:

String 類型的引用變數 a 保留了一個字元串對象 string 的引用,就如同下圖所示,箭頭則表示了變數 a 與真正 String 對象的引用關係。

再通過上述程式碼,我們將變數 a 賦值給變數 b ,變數 b 也存儲了字元串對象 string的引用,它們指向的是同一個對象。

當我們嘗試對變數 a 重新賦值,看下對變數 b 會不會有影響呢

想必小夥伴一看就知道,列印的結果肯定是 string2,string(圖片有誤,應該是a=string2)同樣用畫圖的方式展示這兩個變數與字元串對象的引用關係。

將變數 a 重新賦值後,保存了新的引用,而不是直接在原有的字元串對象上進行數據改變,同時變數 b 仍然存的是對象 string 的引用,變數 ab 兩者相互獨立,不影響,這也正是說明了 String 對象的不可變。

在這裡初認 Java 的小夥伴還可能會有些困惑:對一個String對象 a 賦值 string,然後又讓 a 值為 string2,這個時候a的值變成 了string2, a 的值改變了,為什麼還說 String 對象不可變呢。

其實問題也很簡單,這裡的 a 只是存儲 String 對象的引用,並不是對象本身,a 存儲的是指向對象所在記憶體的地址引用罷了,當第二次賦值時,a 引用指向了對象 string2的記憶體地址,而對象 string2 是重新創建的,之前的 string 對象仍在記憶體中,並且由變數 b 引用著。

除此之外,String 類的返回 String 對象的方法不會改變自身,都是返回一個新的 String 對象來實現,比如 concatreplacesubstring 等等。

為什麼 String 需要不可變

聊完什麼是 String 的不可變後,接下來我們再說說 String 為什麼需要不可變呢,又有什麼好處呢?

字元串常量池的實現

在Java中,我們通常有兩種方式創建字元串對象,一種是通過字元串字面量方式創建,就如上文的程式碼,另外一種就是通過 new 方式去創建,如 String c = new String("string 3"); 而兩者區別就在於通過字元串字面量的方式創建時,JVM 會現在字元串池中檢查字元串內容是否已經存在,如果存在就會直接返回對應的引用,而不是再次分配記憶體進行創建,如果不存在就會分配在記憶體中創建的同時將字元串數據快取在字元串池中,便於重用。正是是由於字元串的不可變,同樣的字元串內容可以讓 JVM 可以減少額外的記憶體分配操作,直接使用在字元串池中字元串對象即可,對性能提升和記憶體節省都大有好處。

關於字元串池,這裡稍微簡單介紹一下:Java 的字元串池屬於 JVM 專門給指定的特殊記憶體區域,用來存儲字元串字面量。在 Java 7 之前,分配於 JVM 的方法區內,屬於常量池的一部分;而 Java7 之後字元串池被移至堆記憶體進行管理,這樣的好處就是允許被 JVM 進行垃圾回收操作,將未被引用的字元串所佔記憶體即使回收,以此節省記憶體。

Hashcode 快取

字元串作為基礎的數據結構,大量地應用在一些集合容器之中,尤其是一些散列集合,在散列集合中,存放元素都要根據對象的 hashCode() 方法來確定元素的位置。由於字元串 hashcode 屬性不會變更,保證了唯一性,使得類似 HashMap,HashSet 等容器才能實現相應的快取功能。由於 String 的不可變,避免重複計算 hashcode,只有使用快取的 hashcode 即可,這樣一來大大提高了在散列集合中使用 String 對象的性能。

執行緒安全

在多執行緒中,只有不變的對象和值是執行緒安全的,可以在多個執行緒中共享數據。由於 String 天然的不可變,當一個執行緒」修改「了字元串的值,只會產生一個新的字元串對象,不會對其他執行緒的訪問產生副作用,訪問的都是同樣的字元串數據,不需要任何同步操作。

安全性

由於字元串無論在任何 Java 系統中都廣泛使用,會用來存儲敏感資訊,如帳號,密碼,網路路徑,文件處理等場景里,保證字元串 String 類的安全性就尤為重要了,如果字元串是可變的,容易被篡改,那我們就無法保證使用字元串進行操作時,它是安全的,很有可能出現 SQL 注入,訪問危險文件等操作。

結語

通過本文,我們介紹 String 是不可變的,可以將它們的引用可以被當作一個普通的變數來使用,無論是在方法間,還是執行緒間傳遞它們,都不用擔心它指向的實際 String 對象發生改變,並且不可變的特性也在語言層面和程式層面上帶了許多好處,我們也應該在編程實踐中多學習效仿,用 James Gosling,Java之父的話說就是,「我會儘可能地使用不可變對象」。

參考資料

  • Diagram to show Java String』s Immutability:https://www.programcreek.com/2009/02/diagram-to-show-java-strings-immutability/
  • Why String is Immutable in Java:https://www.baeldung.com/java-string-immutable
  • Guide to Java String Pool:https://www.baeldung.com/java-string-pool
  • Why String is immutable in Java:https://www.programcreek.com/2013/04/why-string-is-immutable-in-java/
  • The Structure of the Java Virtual Machine:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html