計算機程序的思惟邏輯 (29) - 剖析String

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

上節介紹了單個字符的封裝類Character,本節介紹字符串類。字符串操做大概是計算機程序中最多見的操做了,Java中表示字符串的類是String,本節就來詳細介紹String。java

字符串的基本使用是比較簡單直接的,咱們來看下。正則表達式

基本用法

能夠經過常量定義String變量編程

String name = "老馬說編程";
複製代碼

也能夠經過new建立Stringc#

String name = new String("老馬說編程");
複製代碼

String能夠直接使用+和+=運算符,如:windows

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

輸出爲:數組

老馬說編程,探索編程本質
複製代碼

String類包括不少方法,以方便操做字符串。緩存

判斷字符串是否爲空安全

public boolean isEmpty() 複製代碼

獲取字符串長度bash

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方法,代碼以下:(若是用掘金app看,可能會有亂碼,是掘金bug,能夠經過掘金PC版查看,或者關注個人微信公衆號"老馬說編程"查看)

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編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索