Java String 物件,你瞭解多少?
String 物件的實現
String物件是 Java 中使用最頻繁的物件之一,所以 Java 公司也在不斷的對String物件的實現進行最佳化,以便提升String物件的效能,看下面這張圖,一起了解一下String物件的最佳化過程。
1. 在 Java6 以及之前的版本中
String物件是對 char 陣列進行了封裝實現的物件,主要有四個成員變數:char 陣列、偏移量 offset、字元數量 count、雜湊值 hash。
String物件是透過 offset 和 count 兩個屬性來定位 char[] 陣列,獲取字串。這麼做可以高效、快速地共享陣列物件,同時節省記憶體空間,但這種方式很有可能會導致記憶體洩漏。
2. 從 Java7 版本開始到 Java8 版本
從 Java7 版本開始,Java 對String類做了一些改變。String類中不再有 offset 和 count 兩個變數了。這樣的好處是String物件佔用的記憶體稍微少了些,同時 String.substring 方法也不再共享 char[],從而解決了使用該方法可能導致的記憶體洩漏問題。
3. 從 Java9 版本開始
將 char[] 陣列改為了 byte[] 陣列,為什麼需要這樣做呢?我們知道 char 是兩個位元組,如果用來存一個位元組的字元有點浪費,為了節約空間,Java 公司就改成了一個位元組的byte來儲存字串。這樣在儲存一個位元組的字元是就避免了浪費。
在 Java9 維護了一個新的屬性 coder,它是編碼格式的標識,在計算字串長度或者呼叫 indexOf() 函式時,需要根據這個欄位,判斷如何計算字串長度。coder 屬性預設有 0 和 1 兩個值, 0 代表Latin-1(單位元組編碼),1 代表 UTF-16 編碼。如果 String判斷字串只包含了 Latin-1,則 coder 屬性值為 0 ,反之則為 1。
String 物件的建立方式
1、透過字串常量的方式
String str= "pingtouge"的形式,使用這種形式建立字串時, JVM 會在字串常量池中先檢查是否存在該物件,如果存在,返回該物件的引用地址,如果不存在,則在字串常量池中建立該字串物件並且返回引用。使用這種方式建立的好處是:避免了相同值的字串重複建立,節約了記憶體。
2、String()建構函式的方式
String str = new String("pingtouge")的形式,使用這種方式建立字串物件過程就比較複雜,分成兩個階段,首先在編譯時,字串pingtouge會被加入到常量結構中,類載入時候就會在常量池中建立該字串。然後就是在呼叫new()時,JVM 將會呼叫String的建構函式,同時引用常量池中的pingtouge字串,在堆記憶體中建立一個String物件並且返回堆中的引用地址。
瞭解了String物件兩種建立方式,我們來分析一下下面這段程式碼,加深我們對這兩種方式的理解,下面這段程式碼片中,str是否等於str1呢?
String str = "pingtouge"; String str1 = new String("pingtouge"); system.out.println(str==str1)
我們逐一來分析這幾行程式碼,首先從String str = "pingtouge"開始,這裡使用了字串常量的方式建立字串物件,在建立pingtouge字串物件時,JVM會去常量池中查詢是否存在該字串,這裡的答案肯定是沒有的,所以JVM將會在常量池中建立該字串物件並且返回物件的地址引用,所以str指向的是pingtouge字串物件在常量池中的地址引用。
然後是String str1 = new String("pingtouge")這行程式碼,這裡使用的是建構函式的方式建立字串物件,根據我們上面對建構函式方式建立字串物件的理解,str1得到的應該是堆中pingtouge字串的引用地址。由於str指向的是pingtouge字串物件在常量池中的地址引用而str1指向的是堆中pingtouge字串的引用地址,所以str肯定不等於str1。
String 物件的不可變性
從我們知道String物件的那一刻起,我想大家都知道了String物件是不可變的。那它不可變是怎麼做到的呢?Java 這麼做能帶來哪些好處?我們一起來簡單的探討一下,先來看看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 /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; }
從這段原始碼中可以看出,String類用了 final 修飾符,我們知道當一個類被 final 修飾時,表明這個類不能被繼承,所以String類不能被繼承。這是String不可變的第一點
再往下看,用來儲存字串的char value[]陣列被private 和final修飾,我們知道對於一個被final的基本資料型別的變數,則其數值一旦在初始化之後便不能更改。這是String不可變的第二點。
Java 公司為什麼要將String設定成不可變的,主要從以下三方面考慮:
1、保證 String 物件的安全性。假設 String 物件是可變的,那麼 String 物件將可能被惡意修改。
2、保證 hash 屬性值不會頻繁變更,確保了唯一性,使得類似 HashMap 容器才能實現相應的 key-value 快取功能。
3、可以實現字串常量池
String 物件的最佳化
字串是我們常用的Java型別之一,所以對字串的操作也是避免不了的,在對字串的操作過程中,如果使用不當,效能會天差地別。那麼在字串的操作過程中,有哪些地方需要我們注意呢?
優雅的拼接字串
字串的拼接是對字串操作使用最頻繁的操作之一,由於我們知道String物件的不可變性,所以我們在做拼接時儘可能少的使用+進行字串拼接或者說潛意識裡認為不能使用+進行字串拼接,認為使用+進行字串拼接會產生許多無用的物件。事實真的是這樣嗎?我們來做一個實驗。我們使用+來拼接下面這段字串。
String str8 = "ping" +"tou"+"ge";
一起來分析一下這段程式碼會產生多少個物件?如果按照我們理解的意思來分析的話,首先會建立ping物件,然後建立pingtou物件,最後才會建立pingtouge物件,一共建立了三個物件。真的是這樣嗎?其實不是這樣的,Java 公司怕我們程式設計師手誤,所以對編譯器進行了最佳化,上面的這段字串拼接會被我們的編譯器最佳化,最佳化成一個String str8 = "pingtouge";物件。除了對常量字串拼接做了最佳化以外,對於使用+號動態拼接字串,編譯器也做了相應的最佳化,以便提升String的效能,例如下面這段程式碼:
String str = "pingtouge"; for(int i=0; i<1000; i++) { str = str + i; }
編譯器會幫我們最佳化成這樣:
String str = "pingtouge"; for(int i=0; i<1000; i++) { str = (new StringBuilder(String.valueOf(str))).append(i).toString(); }
可以看出 Java 公司對這一塊進行了不少的最佳化,防止由於程式設計師不小心導致String效能急速下降,儘管 Java 公司在編譯器這一塊做了相應的最佳化,但是我們還是能看出 Java 公司最佳化的不足之處,在動態拼接字串時,雖然使用了 StringBuilder 進行字串拼接,但是每次迴圈都會生成一個新的 StringBuilder 例項,同樣也會降低系統的效能。
所以我們在做字串拼接時,我們需要從程式碼的層面進行最佳化,在動態的拼接字串時,如果不涉及到執行緒安全的情況下,我們顯示的使用 StringBuilder 進行拼接,提升系統效能,如果涉及到執行緒安全的話,我們使用 StringBuffer 來進行字串拼接
巧妙的使用 intern() 方法
* <p> * When the intern method is invoked, if the pool already contains a * string equal to this {@code String} object as determined by * the {@link #equals(Object)} method, then the string from the pool is * returned. Otherwise, this {@code String} object is added to the * pool and a reference to this {@code String} object is returned. * <p> public native String intern();
這是 intern() 函式的官方註釋說明,大概意思就是 intern 函式用來返回常量池中的某字串,如果常量池中已經存在該字串,則直接返回常量池中該物件的引用。否則,在常量池中加入該物件,然後 返回引用。
有一位Twitter工程師在QCon全球軟體開發大會上分享了一個他們對 String物件最佳化的案例,他們利用String.intern()方法將以前需要20G記憶體儲存最佳化到只需要幾百兆記憶體。這足以體現String.intern()的威力,我們一起來看一個例子,簡單的瞭解一下String.intern()的用法。
public static void main(String[] args) { String str = new String("pingtouge"); String str1 = new String("pingtouge"); System.out.println("未使用intern()方法:"+(str==str1)); System.out.println("未使用intern()方法,str:"+str); System.out.println("未使用intern()方法,str1:"+str1); String str2= new String("pingtouge").intern(); String str3 = new String("pingtouge").intern(); System.out.println("使用intern()方法:"+(str2==str3)); System.out.println("使用intern()方法,str2:"+str2); System.out.println("使用intern()方法,str3:"+str3); }
從結果中可以看出,未使用String.intern()方法時,構造相同值的字串物件返回不同的物件引用地址,使用String.intern()方法後,構造相同值的字串物件時,返回相同的物件引用地址。這能幫我們節約不少空間。
String.intern()方法雖然好,但是我們要結合場景使用,不能亂用,因為常量池的實現是類似於一個HashTable的實現方式,HashTable 儲存的資料越大,遍歷的時間複雜度就會增加。如果資料過大,會增加整個字串常量池的負擔。
靈活的字串的分割
字串的分割是字串操作的常用操作之一,對於字串的分割,大部分人使用的都是 Split() 方法,Split() 方法大多數情況下使用的是正規表示式,這種分割方式本身沒有什麼問題,但是由於正規表示式的效能是非常不穩定的,使用不恰當會引起回溯問題,很可能導致 CPU 居高不下。在以下兩種情況下 Split() 方法不會使用正規表示式:
- 傳入的引數長度為1,且不包含“.$|()[{^?*+\”regex元字元的情況下,不會使用正規表示式
- 傳入的引數長度為2,第一個字元是反斜槓,並且第二個字元不是ASCII數字或ASCII字母的情況下,不會使用正規表示式
所以我們在字串分割時,應該慎重使用 Split() 方法,首先考慮使用 String.indexOf() 方法進行字串分割,如果 String.indexOf() 無法滿足分割要求,再使用 Split() 方法,使用 Split() 方法分割字串時,需要注意回溯問題。
文章不足之處,望大家多多指點,共同學習,共同進步
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69923331/viewspace-2661096/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- java異常你瞭解多少Java
- ava String 物件,你真的瞭解了嗎?物件
- 當紅“Serverless”,你瞭解多少?Server
- 關於Synchronized你瞭解多少?synchronized
- 關於繼承,你瞭解多少?繼承
- 抽象類和介面,你瞭解多少?抽象
- .NET中的字串你瞭解多少?字串
- 你對position的瞭解有多少?
- 面試-關於Http協議你瞭解多少,有多少說多少面試HTTP協議
- 商城系統原始碼你瞭解多少?原始碼
- 關於區塊鏈你瞭解多少區塊鏈
- HTTP專業術語,你瞭解多少?HTTP
- 區塊鏈價值你瞭解多少?區塊鏈
- 面試必問之 CopyOnWriteArrayList,你瞭解多少?面試
- Android效能優化你瞭解多少Android優化
- JDK8新特性-你瞭解多少JDK
- JDK9新特性-你瞭解多少JDK
- JDK10新特性-你瞭解多少JDK
- 直流負載的案例,你瞭解多少?負載
- 面試必問的volatile,你瞭解多少?面試
- Android Studio3.3你瞭解多少?Android
- Python 的技巧和方法你瞭解多少?Python
- 你對CommonJS規範瞭解多少?JS
- 對Docker的瞭解,你能讀懂多少?Docker
- 細粒度授權二三事,你瞭解多少?
- 你有認真瞭解過自己的“Java物件”嗎? 渣男Java物件
- 你瞭解Java反射嗎?Java反射
- 關於Linux知識你瞭解多少呢?Linux
- GO 語言的併發模式你瞭解多少?Go模式
- 你對Linux瞭解多少?看看不吃虧!Linux
- 關於Mysql資料儲存,你瞭解多少?MySql
- JS錯誤監控 上報後臺你瞭解多少?JS
- 你瞭解免費OA辦公系統有多少?
- 關於Linux你瞭解多少?Linux由來!Linux
- 對於網校系統原始碼,你瞭解多少?原始碼
- 這些深度學習術語,你瞭解多少?(上)深度學習
- 這些深度學習術語,你瞭解多少?(下)深度學習
- 作為前端的你瞭解多少tcp的內容前端TCP