電腦程式的思維邏輯 (29) - 剖析String

来源:http://www.cnblogs.com/swiftma/archive/2016/08/01/5724504.html
-Advertisement-
Play Games

字元串操作大概是電腦程式中最常見的操作了,Java中表示字元串的類是String,它有哪些方法?內部是如何實現的?如何處理各種不同的編碼?不可變性意味著什麼?字元串常量到底是什麼?hashCode是如何實現的?... ...


上節介紹了單個字元的封裝類Character,本節介紹字元串類。字元串操作大概是電腦程式中最常見的操作了,Java中表示字元串的類是String,本節就來詳細介紹String。

字元串的基本使用是比較簡單直接的,我們來看下。

基本用法

可以通過常量定義String變數

String name = "老馬說編程";

也可以通過new創建String

String name = new String("老馬說編程");

String可以直接使用+和+=運算符,如:

String name = "老馬";
name+= "說編程";
String descritpion = ",探索編程本質";
System.out.println(name+descritpion); 

輸出為:老馬說編程,探索編程本質

String類包括很多方法,以方便操作字元串。

判斷字元串是否為空

public boolean isEmpty()

獲取字元串長度

 public int length()

取子字元串

public String substring(int beginIndex)
public String substring(int beginIndex, int endIndex) 

在字元串中查找字元或子字元串,返回第一個找到的索引位置,沒找到返回-1

public int indexOf(int ch)
public int indexOf(String str)

從後面查找字元或子字元串,返回從後面數的第一個索引位置,沒找到返回-1

public int lastIndexOf(int ch)
public int lastIndexOf(String str) 

判斷字元串中是否包含指定的字元序列。回顧一下,CharSequence是一個介面,String也實現了CharSequence

public boolean contains(CharSequence s)  

判斷字元串是否以給定子字元串開頭

public boolean startsWith(String prefix)

判斷字元串是否以給定子字元串結尾

public boolean endsWith(String suffix)

與其他字元串比較,看內容是否相同

public boolean equals(Object anObject)

忽略大小寫,與其他字元串進行比較,看內容是否相同

public boolean equalsIgnoreCase(String anotherString)

String也實現了Comparable介面,可以比較字元串大小

public int compareTo(String anotherString)

還可以忽略大小寫,進行大小比較

public int compareToIgnoreCase(String str)

所有字元轉換為大寫字元,返回新字元串,原字元串不變

public String toUpperCase()

所有字元轉換為小寫字元,返回新字元串,原字元串不變

public String toLowerCase()

字元串連接,返回當前字元串和參數字元串合併後的字元串,原字元串不變

public String concat(String str)

字元串替換,替換單個字元,返回新字元串,原字元串不變

public String replace(char oldChar, char newChar)

字元串替換,替換字元序列,返回新字元串,原字元串不變

public String replace(CharSequence target, CharSequence replacement) 

刪掉開頭和結尾的空格,返回新字元串,原字元串不變

public String trim() 

分隔字元串,返回分隔後的子字元串數組,原字元串不變

public String[] split(String regex)

例如,按逗號分隔"hello,world":

String str = "hello,world";
String[] arr = str.split(",");

arr[0]為"hello", arr[1]為"world"。

從調用者的角度理解了String的基本用法,下麵我們進一步來理解String的內部。

走進String內部

封裝字元數組

String類內部用一個字元數組表示字元串,實例變數定義為:

private final char value[];

String有兩個構造方法,可以根據char數組創建String

public String(char value[])
public String(char value[], int offset, int count)

需要說明的是,String會根據參數新創建一個數組,並拷貝內容,而不會直接用參數中的字元數組。

String中的大部分方法,內部也都是操作的這個字元數組。比如說:

  • length()方法返回的就是這個數組的長度
  • substring()方法就是根據參數,調用構造方法String(char value[], int offset, int count)新建了一個字元串
  • indexOf查找字元或子字元串時就是在這個數組中進行查找

這些方法的實現大多比較直接,我們就不贅述了。

String中還有一些方法,與這個char數組有關:

返回指定索引位置的char

public char charAt(int index)

返回字元串對應的char數組

public char[] toCharArray()

註意,返回的是一個拷貝後的數組,而不是原數組。

將char數組中指定範圍的字元拷貝入目標數組指定位置

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) 

按Code Point處理字元

與Character類似,String類也提供了一些方法,按Code Point對字元串進行處理。

public int codePointAt(int index)
public int codePointBefore(int index)
public int codePointCount(int beginIndex, int endIndex)
public int offsetByCodePoints(int index, int codePointOffset)

這些方法與我們在剖析Character一節介紹的非常類似,本節就不再贅述了。

編碼轉換

String內部是按UTF-16BE處理字元的,對BMP字元,使用一個char,兩個位元組,對於增補字元,使用兩個char,四個位元組。我們在第六節介紹過各種編碼,不同編碼可能用於不同的字元集,使用不同的位元組數目,和不同的二進位表示。如何處理這些不同的編碼呢?這些編碼與Java內部表示之間如何相互轉換呢?

Java使用Charset這個類表示各種編碼,它有兩個常用靜態方法:

public static Charset defaultCharset()
public static Charset forName(String charsetName) 

第一個方法返回系統的預設編碼,比如,在我的電腦上,執行如下語句:

System.out.println(Charset.defaultCharset().name());

輸出為UTF-8

第二方法返回給定編碼名稱的Charset對象,與我們在第六節介紹的編碼相對應,其charset名稱可以是:US-ASCII, ISO-8859-1, windows-1252, GB2312, GBK, GB18030, Big5, UTF-8,比如:

Charset charset = Charset.forName("GB18030");

String類提供瞭如下方法,返回字元串按給定編碼的位元組表示:

public byte[] getBytes()  
public byte[] getBytes(String charsetName)
public byte[] getBytes(Charset charset) 

第一個方法沒有編碼參數,使用系統預設編碼,第二方法參數為編碼名稱,第三個為Charset。

String類有如下構造方法,可以根據位元組和編碼創建字元串,也就是說,根據給定編碼的位元組表示,創建Java的內部表示。

public String(byte bytes[])
public String(byte bytes[], int offset, int length)
public String(byte bytes[], int offset, int length, String charsetName)
public String(byte bytes[], int offset, int length, Charset charset)
public String(byte bytes[], String charsetName)
public String(byte bytes[], Charset charset)

除了通過String中的方法進行編碼轉換,Charset類中也有一些方法進行編碼/解碼,本節就不介紹了。重要的是認識到,Java的內部表示與各種編碼是不同的,但可以相互轉換。

不可變性

與包裝類類似,String類也是不可變類,即對象一旦創建,就沒有辦法修改了。String類也聲明為了final,不能被繼承,內部char數組value也是final的,初始化後就不能再變了。

String類中提供了很多看似修改的方法,其實是通過創建新的String對象來實現的,原來的String對象不會被修改。比如說,我們來看concat()方法的代碼:

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == 0) {
        return this;
    }
    int len = value.length;
    char buf[] = Arrays.copyOf(value, len + otherLen);
    str.getChars(buf, len);
    return new String(buf, true);
}

通過Arrays.copyOf方法創建了一塊新的字元數組,拷貝原內容,然後通過new創建了一個新的String。關於Arrays類,我們將在後續章節詳細介紹。

與包裝類類似,定義為不可變類,程式可以更為簡單、安全、容易理解。但如果頻繁修改字元串,而每次修改都新建一個字元串,性能太低,這時,應該考慮Java中的另兩個類StringBuilder和StringBuffer,我們在下節介紹它們。

常量字元串

Java中的字元串常量是非常特殊的,除了可以直接賦值給String變數外,它自己就像一個String類型的對象一樣,可以直接調用String的各種方法。我們來看代碼:

System.out.println("老馬說編程".length());
System.out.println("老馬說編程".contains("老馬"));
System.out.println("老馬說編程".indexOf("編程"));

實際上,這些常量就是String類型的對象,在記憶體中,它們被放在一個共用的地方,這個地方稱為字元串常量池,它保存所有的常量字元串,每個常量只會保存一份,被所有使用者共用。當通過常量的形式使用一個字元串的時候,使用的就是常量池中的那個對應的String類型的對象。

比如說,我們來看代碼:

String name1 = "老馬說編程";
String name2 = "老馬說編程";
System.out.println(name1==name2);

輸出為true,為什麼呢?可以認為,"老馬說編程"在常量池中有一個對應的String類型的對象,我們假定名稱為laoma,上面代碼實際上就類似於:

String laoma = new String(new char[]{'老','馬','說','編','程'});
String name1 = laoma;
String name2 = laoma;
System.out.println(name1==name2);

實際上只有一個String對象,三個變數都指向這個對象,name1==name2也就不言而喻了。

需要註意的是,如果不是通過常量直接賦值,而是通過new創建的,==就不會返回true了,看下麵代碼:

String name1 = new String("老馬說編程");
String name2 = new String("老馬說編程");
System.out.println(name1==name2);

輸出為false,為什麼呢?上面代碼類似於:

String laoma = new String(new char[]{'老','馬','說','編','程'});
String name1 = new String(laoma);
String name2 = new String(laoma);
System.out.println(name1==name2);

String類中以String為參數的構造方法代碼如下:

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

hash是String類中另一個實例變數,表示緩存的hashCode值,我們待會介紹。

可以看出, name1和name2指向兩個不同的String對象,只是這兩個對象內部的value值指向相同的char數組。其記憶體佈局大概如下所示:


所以,name1==name2是不成立的,但name1.equals(name2)是true。

hashCode

我們剛剛提到hash這個實例變數,它的定義如下:

private int hash; // Default to 0

它緩存了hashCode()方法的值,也就是說,第一次調用hashCode()的時候,會把結果保存在hash這個變數中,以後再調用就直接返回保存的值。

我們來看下String類的hashCode方法,代碼如下:

public int hashCode() {
    int h = hash;
    if (h == 0 && value.length > 0) {
        char val[] = value;

        for (int i = 0; i < value.length; i++) {
            h = 31 * h + val[i];
        }
        hash = h;
    }
    return h;
}

如果緩存的hash不為0,就直接返回了,否則根據字元數組中的內容計算hash,計算方法是:

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]

s表示字元串,s[0]表示第一個字元,n表示字元串長度,s[0]*31^(n-1)表示31的n-1次方再乘以第一個字元的值。

為什麼要用這個計算方法呢?這個式子中,hash值與每個字元的值有關,每個位置乘以不同的值,hash值與每個字元的位置也有關。使用31大概是因為兩個原因,一方面可以產生更分散的散列,即不同字元串hash值也一般不同,另一方面計算效率比較高,31*h與32*h-h即 (h<<5)-h等價,可以用更高效率的移位和減法操作代替乘法操作。

在Java中,普遍採用以上思路來實現hashCode。

正則表達式

String類中,有一些方法接受的不是普通的字元串參數,而是正則表達式,什麼是正則表達式呢?它可以理解為一個字元串,但表達的是一個規則,一般用於文本的匹配、查找、替換等,正則表達式有著豐富和強大的功能,是一個比較龐大的話題,我們將在後續章節單獨介紹。

Java中有專門的類如Pattern和Matcher用於正則表達式,但對於簡單的情況,String類提供了更為簡潔的操作,String中接受正則表達式的方法有:

分隔字元串

public String[] split(String regex) 

檢查是否匹配

public boolean matches(String regex)

字元串替換

public String replaceFirst(String regex, String replacement)
public String replaceAll(String regex, String replacement) 

小結

本節,我們介紹了String類,介紹了其基本用法,內部實現,編碼轉換,分析了其不可變性,常量字元串,以及hashCode的實現。

本節中,我們提到,在頻繁的字元串修改操作中,String類效率比較低,我們提到了StringBuilder和StringBuffer類。我們也看到String可以直接使用+和+=進行操作,它們的背後也是StringBuilder類。

讓我們下節來看下這兩個類。

----------------

未完待續,查看最新文章,敬請關註微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及電腦技術的本質。用心寫作,原創文章,保留所有版權。

-----------

相關好評原創文章

電腦程式的思維邏輯 (6) - 如何從亂碼中恢復 (上)?

電腦程式的思維邏輯 (7) - 如何從亂碼中恢復 (下)?

電腦程式的思維邏輯 (8) - char的真正含義

電腦程式的思維邏輯 (28) - 剖析包裝類 (下)

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、FileStream的基礎知識 屬性: CanRead 判斷當前流是否支持讀取,返回bool值,True表示可以讀取 CanWrite 判斷當前流是否支持寫入,返回bool值,True表示可以寫入 方法: Read() 從流中讀取數據,返回位元組數組 Write() 將位元組塊(位元組數組)寫入該流 ...
  • 1 /// <summary> 2 /// 九宮格演算法 3 /// </summary> 4 public void NineTable() 5 { 6 //創建一個三階方陣 7 int[,] arr = new int[3, 3]; 8 //第3行的行下標 9 int a = 2; 10 //第2 ...
  • 說明:本文是個人翻譯文章,由於個人水平有限,有不對的地方請大家幫忙更正。 原文: "dotnet run" 翻譯: "dotnet run" 名稱 dotnet run 沒有任何明確的編譯或啟動命令運行“就地”(即運行命令的目錄)源代碼。 概要 `dotnet run [ framework] [ ...
  • 發現很少有集中討論C#可變性限制的中文博文(要麼就是一大段文字中夾雜很多凌亂的部分),所以寫發篇博文,集中討論,這些限制基本是基於安全考慮,亦或者根本難以實現而產生的。 註:本文不再解釋什麼是可變性,以及本文所討論的問題都基於.NET 4至.NET 4.5。所有地方我都力求簡潔。 好了,廢話不說了, ...
  • 隨著互聯網項目用戶訪問量不斷上升,單點web伺服器是無法滿足大型高併發高負載的業務處理的,為了給web伺服器做負載均衡方案,打算採用Nginx搭建負載均衡伺服器,把用戶請求分配到N個伺服器來緩解伺服器壓力。 Nginx簡介: Nginx (“engine x”) 是一個高性能的 HTTP 和 反向代 ...
  • 前段時間,一直有練習ASP.NET MVC與Web API交互,接下來,Insus.NET再做一些相關的練習,Web API與文件操作,如POST文件至Web API,更新或是刪除等。不管怎樣,先在資料庫創建一張表,用來存儲上傳的文件。本實例中是把文件存儲過資料庫的。 CREATE TABLE Ap ...
  • 1.continue,break,ruturn eg:1-100的和 結果為:5050 換為break,查看結果 結果為:10 結論一:break:跳出整個迴圈體 換為continue看一下結果又是多少? 結果為:5045,(除5之外都執行) 結論二:continue跳過當前條件的迴圈 return ...
  • 目錄索引 【無私分享:ASP.NET CORE 項目實戰】目錄索引 簡介 本章我們來介紹下Asp.net Core 使用 CodeFirst 創建資料庫和表,通過 控制台 和 dotnet ef 兩種方式 修改EF上下文對象,添加測試類 我修改了一下名字,Domains 改為了 wkmvc.Data ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...