自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

面試常備,字符串三劍客 String、StringBuffer、StringBuilder

開發(fā) 后端
字符串操作毫無疑問是計算機(jī)程序設(shè)計中最常見的行為之一,在 Java 大展拳腳的 Web 系統(tǒng)中更是如此。

[[382597]]

本文轉(zhuǎn)載自微信公眾號「飛天小牛肉」,作者飛天小牛肉。轉(zhuǎn)載本文請聯(lián)系飛天小牛肉公眾號。

字符串操作毫無疑問是計算機(jī)程序設(shè)計中最常見的行為之一,在 Java 大展拳腳的 Web 系統(tǒng)中更是如此。

全文脈絡(luò)思維導(dǎo)圖如下:

1. 三劍客之首:不可變的 String

概述

 

「Java 沒有內(nèi)置的字符串類型」, 而是在標(biāo)準(zhǔn) Java 類庫中提供了一個「預(yù)定義類」 String。每個用「雙引號括起來的字符串都是 String 類的一個實(shí)例」:

  1. String e = ""; // 空串 
  2. String str = "hello"

看一下 String 的源碼,「在 Java 8 中,String 內(nèi)部是使用 char 數(shù)組來存儲數(shù)據(jù)的」。

  1. public final class String 
  2.     implements java.io.Serializable, Comparable<String>, CharSequence { 
  3.     /** The value is used for character storage. */ 
  4.     private final char value[]; 

可以看到,String 類是被 final 修飾的,因此 「String 類不允許被繼承」。

「而在 Java 9 之后,String 類的實(shí)現(xiàn)改用 byte 數(shù)組存儲字符串」,同時使用 coder 來標(biāo)識使用了哪種編碼。

  1. public final class String 
  2.     implements java.io.Serializable, Comparable<String>, CharSequence { 
  3.     /** The value is used for character storage. */ 
  4.     private final byte[] value; 
  5.  
  6.     /** The identifier of the encoding used to encode the bytes in {@code value}. */ 
  7.     private final byte coder; 

不過,無論是 Java 8 還是 Java 9,「用來存儲數(shù)據(jù)的 char 或者 byte 數(shù)組 value 都一直是被聲明為 final 的」,這意味著 value 數(shù)組初始化之后就不能再引用其它數(shù)組了。并且 String內(nèi)部沒有改變 value 數(shù)組的方法,因此我們就說 String 是不可變的。

所謂不可變,就如同數(shù)字 3 永遠(yuǎn)是數(shù)字 3 —樣,字符串 “hello” 永遠(yuǎn)包含字符 h、e、1、1 和 o 的代碼單元序列, 不能修改其中的任何一個字符。當(dāng)然, 可以修改字符串變量 str, 讓它引用另外一個字符串, 這就如同可以將存放 3 的數(shù)值變量改成存放 4 一樣。

我們看個例子:

  1. String str = "asdf"
  2. String x = str.toUpperCase(); 

toUpperCase 用來將字符串全部轉(zhuǎn)為大寫字符,進(jìn)入 toUpperCase 的源碼我們發(fā)現(xiàn),這個看起來會修改 String 值的方法,實(shí)際上最后是創(chuàng)建了一個全新的 String 對象,而最初的 String 對象則絲毫未動。

 

空串與 Null

空串 "" 很好理解,就是長度為 0 的字符串??梢哉{(diào)用以下代碼檢查一個字符串是否為空:

  1. if(str.length() == 0){ 
  2.     // todo 

或者

  1. if(str.equals("")){ 
  2.  // todo 

「空串是一個 Java 對象」, 有自己的串長度( 0 ) 和內(nèi)容(空),也就是 value 數(shù)組為空。

String變量還可以存放一個特殊的值, 名為 null,這表示「目前沒有任何對象與該變量關(guān)聯(lián)」。要檢查一個字符串是否為 null,可如下判斷:

  1. if(str == null){ 
  2.     // todo 

有時要檢查一個字符串既不是 null也不為空串,這種情況下就需要使用以下條件:

  1. if(str != null && str.length() != 0){ 
  2.     // todo 

有同學(xué)就會覺得,這么簡單的條件判斷還用你說?沒錯,這雖然簡單,但仍然有個小坑,就是我們「必須首先檢查 str 是否為 null,因為如果在一個 null 值上調(diào)用方法,編譯器會報錯」。

字符串拼接

上面既然說到 String 是不可變的,我們來看段代碼,為什么這里的字符串 a 卻發(fā)生了改變?

  1. String a = "hello"
  2. String b = "world"
  3. a = a + b; // a = "helloworld" 

實(shí)際上,在使用 + 進(jìn)行字符串拼接的時候,JVM 是初始化了一個 StringBuilder 來進(jìn)行拼接的。相當(dāng)于編譯后的代碼如下:

  1. String a = "hello"
  2. String b = "world"
  3. StringBuilder builder = new StringBuilder(); 
  4. builder.append(a); 
  5. builder.append(b); 
  6. a = builder.toString(); 

關(guān)于 StringBuilder 下文會詳細(xì)講解,大家現(xiàn)在只需要知道 StringBuilder 是可變的字符串類型就 OK 了。我們看下 builder.toString() 的源碼:

 

顯然,toString方法同樣是生成了一個新的 String 對象,而不是在舊字符串的內(nèi)容上做更改,相當(dāng)于把舊字符串的引用指向的新的String對象。這也就是字符串 a 發(fā)生變化的原因。

另外,我們還需要了解一個特性,當(dāng)將一個字符串與一個非字符串的值進(jìn)行拼接時,后者被自動轉(zhuǎn)換成字符串(「任何一個 Java 對象都可以轉(zhuǎn)換成字符串」)。例如:

  1. int age = 13; 
  2. String rating = "PG" + age; // rating = "PG13" 

這種特性通常用在輸出語句中。例如:

  1. int a = 12; 
  2. System.out.println("a = " + a); 

結(jié)合上面這兩特性,我們來看個小問題,「空串和 null 拼接的結(jié)果是啥」?

  1. String str = null
  2. str = str + ""
  3. System.out.println(str); 

答案是 null 大家應(yīng)該都能猜出來,但為什么是 null 呢?上文說過,使用 + 進(jìn)行拼接實(shí)際上是會轉(zhuǎn)換為 StringBuilder 使用 append 方法進(jìn)行拼接,編譯后的代碼如下:

String str = null;str = str + "";StringBuilder builder = new StringBuilder();builder.append(str);builder.append("");str = builder.toString();

看下 append 的源碼:

 

可以看出,當(dāng)傳入的字符串是 null 時,會調(diào)用 appendNull 方法,而這個方法會返回 null。

檢測字符串是否相等

可以使用 equals方法檢測兩個字符串是否相等。比如:

  1. String str = "hello"
  2. System.out.println("hello".equals(str)); // true 

equals 其實(shí)是 Object 類中的一個方法,所有的類都繼承于 Object 類。講解 equals 方法之前,我們先來回顧一下運(yùn)算符 == 的用法,它存在兩種使用情況:

  • 對于基本數(shù)據(jù)類型來說, == 比較的是值是否相同;
  • 對于引用數(shù)據(jù)類型來說, == 比較的是內(nèi)存地址是否相同。

舉個例子:

  1. String str1 = new String("hello");  
  2. String str2 = new String("hello"); 
  3. System.out.println(str1 == str2); // false 

對 Java 中數(shù)據(jù)存儲區(qū)域仍然不明白的可以先回去看看第一章《萬物皆對象》。對于上述代碼,str1 和 str2 采用構(gòu)造函數(shù) new String() 的方式新建了兩個不同字符串,以 String str1 = new String("hello"); 為例,new 出來的對象存放在堆內(nèi)存中,用一個引用 str1 來指向這個對象的地址,而這個對象的引用 str1 存放在棧內(nèi)存中。str1 和 str2 是兩個不同的對象,地址不同,因此 == 比較的結(jié)果也就為 false。

 

而實(shí)際上,Object 類中的原始 equals 方法內(nèi)部調(diào)用的還是運(yùn)算符 ==,「判斷的是兩個對象是否具有相同的引用(地址),和 == 的效果是一樣的」:

 

也就是說,如果你新建的類沒有覆蓋 equals 方法,那么這個方法比較的就是對象的地址。而 String 方法覆蓋了 equals 方法,我們來看下源碼:

 

可以看出,String 重寫的 equals 方法比較的是對象的內(nèi)容,而非地址。

總結(jié)下 equals()的兩種使用情況:

  • 情況 1:類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價于通過 == 比較這兩個對象(比較的是地址)。
  • 情況 2:類覆蓋了 equals() 方法。一般來說,我們都覆蓋 equals() 方法來判斷兩個對象的內(nèi)容是否相等,比如 String 類就是這樣做的。當(dāng)然,你也可以不這樣做。

舉個例子:

  1. String a = new String("ab"); // a 為一個字符串引用 
  2. String b = new String("ab"); // b 為另一個字符串引用,這倆對象的內(nèi)容一樣 
  3.  
  4. if (a.equals(b)) // true 
  5.     System.out.println("aEQb"); 
  6. if (a == b) // false,不是同一個對象,地址不同 
  7.     System.out.println("a==b"); 

字符串常量池

字符串 String 既然作為 Java 中的一個類,那么它和其他的對象分配一樣,需要耗費(fèi)高昂的時間與空間代價,作為最基礎(chǔ)最常用的數(shù)據(jù)類型,大量頻繁的創(chuàng)建字符串,將會極大程度的影響程序的性能。為此,JVM 為了提高性能和減少內(nèi)存開銷,在實(shí)例化字符串常量的時候進(jìn)行了一些優(yōu)化:

  • 為字符串開辟了一個「字符串常量池 String Pool」,可以理解為緩存區(qū)
  • 創(chuàng)建字符串常量時,首先檢查字符串常量池中是否存在該字符串
  • 「若字符串常量池中存在該字符串,則直接返回該引用實(shí)例,無需重新實(shí)例化」;若不存在,則實(shí)例化該字符串并放入池中。

舉個例子:

  1. String str1 = "hello"
  2. String str2 = "hello"
  3.  
  4. System.out.printl("str1 == str2" : str1 == str2 ) //true  

對于上面這段代碼,String str1 = "hello";, 「編譯器首先會在棧中創(chuàng)建一個變量名為 str1 的引用,然后在字符串常量池中查找有沒有值為 "hello" 的引用,如果沒找到,就在字符串常量池中開辟一個地址存放 "hello" 這個字符串,然后將引用 str1 指向 "hello"」。

 

需要注意的是,字符串常量池的位置在 JDK 1.7 有所變化:

  • 「JDK 1.7 之前」,字符串常量池存在于「常量存儲」(Constant storage)中
  • 「JDK 1.7 之后」,字符串常量池存在于「堆內(nèi)存」(Heap)中。

 

另外,我們還「可以使用 String的 intern()方法在運(yùn)行過程中手動的將字符串添加到 String Pool 中」。具體過程是這樣的:

當(dāng)一個字符串調(diào)用 intern() 方法時,如果 String Pool 中已經(jīng)存在一個字符串和該字符串的值相等,那么就會返回 String Pool 中字符串的引用;否則,就會在 String Pool 中添加一個新的字符串,并返回這個新字符串的引用。

看下面這個例子:

  1. String str1 = new String("hello");  
  2. String str3 = str1.intern(); 
  3. String str4 = str1.intern(); 
  4. System.out.println(str3 == str4); // true 

對于 str3 來說,str1.intern() 會先在 String Pool 中查看是否已經(jīng)存在一個字符串和 str1 的值相等,沒有,于是,在 String Pool 中添加了一個新的值和 str1 相等的字符串,并返回這個新字符串的引用。

而對于 str4 來說,str1.intern()在 String Pool 中找到了一個字符串和 str1 的值相等,于是直接返回這個字符串的引用。因此 s3 和 s4 引用的是同一個字符串,也就是說它們的地址相同,所以 str3 == str4 的結(jié)果是 true。

 

「總結(jié):」

  • String str = "i" 的方式,java 虛擬機(jī)會自動將其分配到常量池中;
  • String str = new String(“i”) 則會被分到堆內(nèi)存中??赏ㄟ^ intern 方法手動加入常量池

new String("hello") 創(chuàng)建了幾個字符串對象

下面這行代碼到底創(chuàng)建了幾個字符串對象?僅僅只在堆中創(chuàng)建了一個?

  1. String str1 = new String("hello");  

顯然不是。對于 str1 來說,new String("hello") 分兩步走:

  • 首先,"hello" 屬于字符串字面量,因此編譯時期會在 String Pool 中查找有沒有值為 "hello" 的引用,如果沒找到,就在字符串常量池中開辟地址空間創(chuàng)建一個字符串對象,指向這個 "hello" 字符串字面量;
  • 然后,使用 new的方式又會在堆中創(chuàng)建一個字符串對象。

因此,使用這種方式一共會創(chuàng)建兩個字符串對象(前提是 String Pool 中還沒有 "hello" 字符串對象)。

2. 雙生子:可變的 StringBuffer 和 StringBuilder

String 字符串拼接問題

 

有些時候, 需要由較短的字符串構(gòu)建字符串, 例如, 按鍵或來自文件中的單詞。采用字符串拼接的方式達(dá)到此目的效率比較低。由于 String 類的對象內(nèi)容不可改變,所以每當(dāng)進(jìn)行字符串拼接時,總是會在內(nèi)存中創(chuàng)建一個新的對象。既耗時, 又浪費(fèi)空間。例如:

  1. String s = "Hello"
  2. s += "World"

這段簡單的代碼其實(shí)總共產(chǎn)生了三個字符串,即 "Hello"、"World" 和 "HelloWorld"。"Hello" 和 "World" 作為字符串常量會在 String Pool 中被創(chuàng)建,而拼接操作 + 會 new 一個對象用來存放 "HelloWorld"。

使用 StringBuilder/ StringBuffer 類就可以避免這個問題的發(fā)生,畢竟 String 的 + 操作底層都是由 StringBuilder 實(shí)現(xiàn)的。StringBuilder 和 StringBuffer 擁有相同的父類:

 

但是,StringBuilder 不是線程安全的,在多線程環(huán)境下使用會出現(xiàn)數(shù)據(jù)不一致的問題,而 StringBuffer 是線程安全的。這是因為在 StringBuffer 類內(nèi),常用的方法都使用了synchronized 關(guān)鍵字進(jìn)行同步,所以是線程安全的。而 StringBuilder 并沒有。這也是運(yùn)行速度 StringBuilder 大于 StringBuffer 的原因了。因此,如果在單線程下,優(yōu)先考慮使用 StringBuilder。

初始化操作

StringBuilder 和 StringBuffer 這兩個類的 API 是相同的,這里就以 StringBuilder 為例演示其初始化操作。

StringBuiler/StringBuffer不能像 String 那樣直接用字符串賦值,所以也不能那樣初始化。它「需要通過構(gòu)造方法來初始化」。首先, 構(gòu)建一個空的字符串構(gòu)建器:

  1. StringBuilder builder = new StringBuilder(); 

當(dāng)每次需要添加一部分內(nèi)容時, 就調(diào)用 append 方法:

  1. char ch = 'a'
  2. builder.append(ch); 
  3.  
  4. String str = "ert" 
  5. builder.append(str); 

在需要構(gòu)建字符串 String 時調(diào)用 toString 方法, 就能得到一個 String對象:

  1. String mystr = builder.toString(); // aert 

3. String、StringBuffer、StringBuilder 比較

  可變性 線程安全
String 不可變 因為不可變,所以是線程安全的
StringBuffer 可變 線程安全的,因為其內(nèi)部大多數(shù)方法都使用 synchronized進(jìn)行同步。其效率較低
StringBuilder 可變 不是線程安全的,因為沒有使用 synchronized進(jìn)行同步,這也是其效率高于 StringBuffer  的原因。單線程下,優(yōu)先考慮使用 StringBuilder。

關(guān)于 synchronized 保證線程安全的問題,我們后續(xù)文章再說。

 

  •  References《Java 核心技術(shù) - 卷 1 基礎(chǔ)知識 - 第 10 版》
  • 《Thinking In Java(Java 編程思想)- 第 4 版》

 

責(zé)任編輯:武曉燕 來源: 飛天小牛肉
相關(guān)推薦

2011-03-28 16:04:44

nagios

2019-06-27 10:06:54

Linux 性能工具

2010-02-04 16:22:21

2017-07-25 08:53:14

CorrectLinkCCA-SD算法

2023-10-04 00:20:31

grepLinux

2024-06-04 00:20:00

Python函數(shù)

2009-02-26 18:22:49

桌面虛擬化Linux

2019-08-20 14:29:45

grepsedawk

2021-05-13 10:25:29

Linuxgrep命令

2018-05-04 15:18:01

DockerDocker Comp容器

2014-11-26 10:18:32

Cloud Setupwindows在線打包工具

2021-03-15 07:39:48

LinuxAwk 語言

2011-07-04 09:07:54

2009-03-19 20:52:58

LinuxPHPCMS

2011-08-06 23:58:34

愛普生投影機(jī)

2023-11-25 17:08:47

ChatbotLLAMALangChain

2011-04-11 11:01:03

AndroidHTC蘋果

2025-04-22 09:39:46

Python爬蟲網(wǎng)頁數(shù)據(jù)抓取

2009-02-12 09:12:27

JPAEJBJSF

2013-08-16 11:14:48

創(chuàng)業(yè)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號